[LON-CAPA-cvs] cvs: doc /loncapafiles loncapafiles.lpml loncom/homework grades.pm loncom/html/res/adm/pages passback.png loncom/interface loncourserespicker.pm

raeburn raeburn at source.lon-capa.org
Tue Dec 3 18:34:12 EST 2024


raeburn		Tue Dec  3 23:34:12 2024 EDT

  Added files:                 
    /loncom/html/res/adm/pages	passback.png 

  Modified files:              
    /loncom/homework	grades.pm 
    /loncom/interface	loncourserespicker.pm 
    /doc/loncapafiles	loncapafiles.lpml 
  Log:
  - Bug 6907. "Content in a course can be set to be deep-link only".
    Course personnel with mgr priv can pass scores back to launcher CMS for
    students who accessed via deep-link with LTI-mediated link protection. 
  
  
-------------- next part --------------
Index: loncom/homework/grades.pm
diff -u loncom/homework/grades.pm:1.795 loncom/homework/grades.pm:1.796
--- loncom/homework/grades.pm:1.795	Mon Jul  1 22:29:01 2024
+++ loncom/homework/grades.pm	Tue Dec  3 23:34:10 2024
@@ -1,7 +1,7 @@
 # The LearningOnline Network with CAPA
 # The LON-CAPA Grading handler
 #
-# $Id: grades.pm,v 1.795 2024/07/01 22:29:01 raeburn Exp $
+# $Id: grades.pm,v 1.796 2024/12/03 23:34:10 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -47,10 +47,12 @@
 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 @@
 #--- 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 @@
     #
     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 @@
                 }
             }
         }
+        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$/, at getsec)) {
@@ -771,7 +798,7 @@
 	}
     }
     my @sections = sort(keys(%sections));
-    return ($classlist,\@sections,\%fullnames);
+    return ($classlist,\@sections,\%fullnames,\%passback);
 }
 
 sub canmodify {
@@ -1034,6 +1061,611 @@
     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 '<p>'.$output.'</p>';
+    }
+    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).
+                  '<p><br />'.&mt('Set criteria to use to list students for possible passback of scores, then push Next [_1]',
+                                  '→').
+                  '</p>';
+    }
+    $result .= '<form action="/adm/grades" method="post" name="gradingMenu">'."\n".
+               '<input type="hidden" name="passback" value="'.&escape($chosen).'" />'."\n".
+               '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\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 .= '<span class="LC_warning">'.&mt('Invalid launcher').'</span>'."\n";
+        } else {
+            $result .= '<span class="LC_warning">'.&mt('No launcher selected').'</span>'."\n";
+        }
+    }
+    $result .=  '<input type="hidden" name="command" value="'.$newcommand.'" />'."\n".
+                '<div>'."\n".
+                '<input type="submit" value="'.$submittext.'" />'."\n".
+                '</div>'."\n".
+                '</form>'."\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(':', at statuses);
+    $result .= '<form action="/adm/grades" method="post" name="passbackusers">'."\n".
+               '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($symb).'" />'."\n";
+    if ($launcher ne '') {
+        $result .= '<input type="hidden" name="passback" value="'.&escape($chosen).'" />'."\n".
+                   '<input type="hidden" name="Status" value="'.$stu_status.'" />'."\n";
+        my ($sections,$groups,$group_display,$disabled) = &sections_and_groups();
+        my $section_display = join(' ',@{$sections});
+        my $status_display;
+        if ((grep(/^Any$/, at statuses)) ||
+            (@statuses == 3)) {
+            $status_display = &mt('Any');
+        } else {
+            $status_display = join(' '.&mt('or').' ',map { &mt($_); } @statuses);
+        }
+        $result .= '<p>'.&mt('Student(s) with stored passback credentials for [_1], and also satisfy:',
+                             '<span class="LC_cusr_emph">'.$linkuri.'</span>').
+                   '<ul>'.
+                   '<li>'.&mt('Section(s)').": $section_display</li>\n".
+                   '<li>'.&mt('Group(s)').": $group_display</li>\n".
+                   '<li>'.&mt('Status').": $status_display</li>\n".
+                   '</ul>';
+        my ($classlist,undef,$fullname) = &getclasslist($sections,'1',$groups,'','','',$chosen);
+        if (keys(%$fullname)) {
+            $newcommand = 'passbackscores';
+            $result .= &build_section_inputs().
+                       &checkselect_js('passbackusers').
+                       '<p><br />'.
+                       &mt("To send scores, check box(es) next to the student's name(s), then push 'Send Scores'.").
+                       '</p>'.
+                       &check_script('passbackusers', 'stuinfo')."\n".
+                       '<input type="button" '."\n".
+                       'onclick="javascript:checkSelect(this.form.stuinfo);" '."\n".
+                       'value="'.&mt('Send Scores').'" /> <br />'."\n".
+                       &check_buttons()."\n".
+                       &Apache::loncommon::start_data_table().
+                       &Apache::loncommon::start_data_table_header_row();
+            my $loop = 0;
+            while ($loop < 2) {
+                $result .= '<th>'.&mt('No.').'</th><th>'.&mt('Select').'</th>'.
+                           '<th>'.&nameUserString('header').' '.&mt('Section/Group').'</th>';
+                $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 .= '<td align="right">'.$ctr.' </td>'.
+                               '<td align="center"><label><input type="checkbox" name="stuinfo" value="'.
+                               $student.':'.$$fullname{$student}.':::SECTION'.$section.
+                               ') " />  </label></td>'."\n".'<td>'.
+                               &nameUserString(undef,$$fullname{$student},$uname,$udom).
+                               ' '.$section.($group ne '' ?'/'.$group:'').'</td>'."\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 .= '<input type="button" '.
+                           'onclick="javascript:checkSelect(this.form.stuinfo);" '.
+                           'value="'.&mt('Send Scores').'" />'."\n";
+            }
+        } else {
+            $submittext = '← '.&mt('Previous');
+            $newcommand = 'passback';
+            $result .= '<span class="LC_warning">'.&mt('No students match the selection criteria').'</p>';
+        }
+    } else {
+        $newcommand = 'initialpassback';
+        $submittext = &mt('Start over');
+        if ($env{'form.passback'}) {
+            $result .= '<span class="LC_warning">'.&mt('Invalid launcher').'</span>'."\n";
+        } else {
+            $result .= '<span class="LC_warning">'.&mt('No launcher selected').'</span>'."\n";
+        }
+    }
+    $result .=  '<input type="hidden" name="command" value="'.$newcommand.'" />'."\n";
+    if (!$ctr) {
+        $result .= '<div>'."\n".
+                   '<input type="submit" value="'.$submittext.'" />'."\n".
+                   '</div>'."\n";
+    }
+    $result .= '</form>'."\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) = &sections_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 .= '<th>'.&mt('No.').'</th>'.
+                                           '<th>'.&nameUserString('header').' '.&mt('Section/Group').'</th>'.
+                                           '<th>'.&mt('Score').'</th>';
+                                 $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 .= '<td align="right">'.$ctr.' </td>'.
+                                                       '<td>'.&nameUserString(undef,$$fullname{$student},$uname,$udom).
+                                                       ' '.$section.($group ne '' ?'/'.$group:'').'</td>'.
+                                                       '<td>'.$score.'</td>'."\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('<p><br />'.&mt('Scores sent to launcher CMS').'</p>'.
+                                                '<p>'.$outcome.'</p>');
+                            } else {
+                                $request->print('<p>'.&mt('No scores sent to launcher CMS').'</p>');
+                            }
+                            if (keys(%tosend)) {
+                                $request->print('<p>'.&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 = '<li>'.&nameUserString(undef,$$fullname{$student},$uname,$udom).'</li>'."\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('<br />'.&mt('Total points possible was 0').':'.
+                                                    '<ul>'.$zeros.'</ul><br />');
+                                }
+                                if ($nopbcreds) {
+                                    $request->print('<br />'.&mt('Missing unique identifier and/or passback location').':'.
+                                                    '<ul>'.$nopbcreds.'</ul><br />');
+                                }
+                                if ($noconfirm) {
+                                    $request->print('<br />'.&mt('Score receipt not confirmed by receiving CMS').':'.
+                                                    '<ul>'.$noconfirm.'</ul><br />');
+                                }
+                                if ($noscore) {
+                                    $request->print('<br />'.&mt('Score computation or transmission failed').':'.
+                                                    '<ul>'.$noscore.'</ul><br />');
+                                }
+                                $request->print('</p>');
+                            }
+                        } 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('<p class="LC_info">'.$error.'</p>');
+    }
+    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, at groups,$group_display);
+    @groups = &Apache::loncommon::get_env_multiple('form.group');
+    if (grep(/^all$/, at groups)) {
+         @groups = ('all');
+         $group_display = 'all';
+    } elsif (grep(/^none$/, at groups)) {
+         @groups = ('none');
+         $group_display = 'none';
+    } elsif (@groups > 0) {
+         $group_display = join(', ', at 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$/, at sections)) {
+            undef($disabled);
+        } else {
+            foreach my $sec (@sections) {
+                if (&canmodify($sec)) {
+                    undef($disabled);
+                    last;
+                }
+            }
+        }
+    }
+    if (grep(/^all$/, at 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 '<p>'.
+           &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().'</p>'."\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 @@
         }
     }
 
-    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(<<LISTJAVASCRIPT));
-    function checkSelect(checkBox) {
-	var ctr=0;
-	var sense="";
-	if (checkBox.length > 1) {
-	    for (var i=0; i<checkBox.length; i++) {
-		if (checkBox[i].checked) {
-		    ctr++;
-		}
-	    }
-	    sense = '$js_lt{'multiple'}';
-	} else {
-	    if (checkBox.checked) {
-		ctr = 1;
-	    }
-	    sense = '$js_lt{'single'}';
-	}
-	if (ctr == 0) {
-	    alert(sense);
-	    return false;
-	}
-	document.gradesub.submit();
-    }
 
     function reLoadList(formname) {
 	if (formname.saveStatusOld.value == pullDownSelection(formname.Status)) {return;}
@@ -1380,7 +1986,55 @@
     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(<<LISTJAVASCRIPT);
+
+    function checkSelect(checkBox) {
+        var ctr=0;
+        var sense="";
+        var len = checkBox.length;
+        if (len == undefined) len = 1;
+        if (len > 1) {
+            for (var i=0; i<len; i++) {
+                if (checkBox[i].checked) {
+                    ctr++;
+                }
+            }
+            sense = '$js_lt{'multiple'}';
+        } else {
+            if (checkBox.checked) {
+                ctr = 1;
+            }
+            sense = '$js_lt{'single'}';
+        }
+        if (ctr == 0) {
+            alert(sense);
+            return false;
+        }
+        document.$formname.submit();
+    }
+LISTJAVASCRIPT
+
+}
 
 sub check_script {
     my ($form,$type) = @_;
@@ -10560,7 +11214,8 @@
 
 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 {
@@ -10669,7 +11324,20 @@
 
                     ]
             });
-
+    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 .= '<form method="post" action="" name="gradingMenu">';
@@ -11889,6 +12557,44 @@
                undef,undef,undef,undef,undef,undef,undef,1);
             $request->print('<div style="padding:0;clear:both;margin:0;border:0"></div>');
             &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('<p class="LC_error">'.&mt('Access Denied ([_1])',$command).'</p>');
Index: loncom/interface/loncourserespicker.pm
diff -u loncom/interface/loncourserespicker.pm:1.18 loncom/interface/loncourserespicker.pm:1.19
--- loncom/interface/loncourserespicker.pm:1.18	Sun Nov 24 04:17:50 2024
+++ loncom/interface/loncourserespicker.pm	Tue Dec  3 23:34:11 2024
@@ -1,6 +1,6 @@
 # The LearningOnline Network
 #
-# $Id: loncourserespicker.pm,v 1.18 2024/11/24 04:17:50 raeburn Exp $
+# $Id: loncourserespicker.pm,v 1.19 2024/12/03 23:34:11 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -33,51 +33,73 @@
 
 =head1 SYNOPSIS
 
-loncourserespicker provides an interface for selecting which folders and/or
-resources are to be either:
+loncourserespicker provides either (1) an interface for selecting which 
+folders and/or resources are to be selected for a specific action, one of: 
 
-(a) exported to an IMS Content Package
-(b) subject to access blocking for the duration of an exam/quiz.
-(c) dumped to an Authoring Space
+(a) export to an IMS Content Package
+(b) be subject to access blocking for the duration of an exam/quiz.
+(c) dump to an Authoring Space
 (d) receive shortened URLs to be used when deep-linking into a course
 
+or (2) an interface for selecting a single folder or resource for which 
+existing passback credentials can be used to send scores to another Course
+Management System (CMS).
+
 =head1 DESCRIPTION
 
 This module provides routines to generate a hierarchical display of folders
-and resources in a course which can be selected for specific actions.
-
-The choice of items is copied back to the main window from which the pop-up
-window used to display the Course Contents was opened.
+and resources in a course which can be selected for specific actions. In 
+all except one use case all items in the course are shown. The case where
+only a filtered list is shown is passback of scores, and filtering limits
+folders and resources to those items for which passback credentials exist,
+(and their parent folders).
+
+When the display is shown in a pop-up window, The choice of items will be
+copied back to the main window from which the pop-up window used to display
+the Course Contents was opened.
 
 =head1 OVERVIEW
 
-The main subroutine: &create_picker() will display the hierarchy of folders,
-sub-folders, and resources in the Main Content area.  Items can be selected
-using checkboxes, and/or a "Check All" button.  Selection of a folder
-causes the contents of the folder to also be selected automatically. The
-propagation of check status is recursive into sub-folders.  Likewise, if an
-item deep in a nested set of folders and sub-folders is unchecked, the 
-uncheck will propagate up through the hierarchy causing any folders at
-a higher level to become unchecked.
+In the cases where multiple items may be selected the main subroutine:
+&create_picker() will display the hierarchy of folders, sub-folders, and
+resources in the Main Content area. Items can be selected using checkboxes,
+and/or a "Check All" button.  Selection of a folder causes the contents of
+the folder to also be selected automatically. The propagation of check
+status is recursive into sub-folders.  Likewise, if an item deep in a nested
+set of folders  and sub-folders is unchecked, the uncheck will propagate up
+through the hierarchy causing any folders at a higher level to become
+unchecked.
+
+In the case where only a single item may be selected the main subroutine:
+&create_picker() will display the hierarchy of folders and sub-folders for
+only those items for which passback credentials exist,
 
 There is a submit button, which will be named differently according to the 
 context in which resource/folder selection is being made.
 
-The four contexts currently supported are: IMS export, selection of
+The five contexts currently supported are: IMS export, selection of
 content to be subject to access restructions for the duration of an
-exam, selection of items for dumping to an Authoring Space, and 
-display or creation of shortened URLs for deep-linking,
+exam, selection of items for dumping to an Authoring Space, display or 
+creation of shortened URLs for deep-linking, and selection of a single
+item for apssback of grades to another CMS.
 
 =head1 INTERNAL SUBROUTINES
 
 =item &create_picker()
 
-Created HTML mark up to display contents of course with checkboxes to
+In the cases where multiple items may be selected ...
+
+Creates HTML markup to display contents of course with checkboxes to
 select items.  Checking a folder causes recursive checking of items
 within the folder. Unchecking a resource causing unchecking of folders
 containing the item back up to the top level.
 
-Inputs: 11.
+In the case where only a single item may be selected ...
+
+Creates HTML markup to display filtered contents of course with radio
+buttons to select an item.
+
+Inputs: 13.
    - $navmap  -- Reference to LON-CAPA navmap object 
                 (encapsulates information about resources in the course). 
 
@@ -113,13 +135,23 @@
    - $tiny -- Reference to hash: keys are symbs of course items for which
               shortened URLs have already been created.
 
+   - $passback -- Reference to hash: keys are symbs of course items for
+                  which passback credentials exist. For each symb the
+                  hash value is itself a hash of deeplink launch items
+                  for that symb with inner hash key set to: 
+                  $linkuri\0$linkprotector\0$scope, and corresponding 
+                  value of 1.
+
    - $readonly -- if true, no "check all" or "uncheck all" buttons will
                   be displayed, and checkboxes will be disabled, if this 
-                  is for an exam block or for shortened URL creation.
+                  is for an exam block or for shortened URL creation,
+                  and radio buttons will be disabled, if this is for
+                  passback of scores to another CMS, 
 
 
 Output: $output is the HTML mark-up for display/selection of content
-        items in the pop-up window.
+        items, either in a pop-up window, or in the main window, 
+        depending on context.
 
 =item &respicker_javascript()
 
@@ -154,7 +186,8 @@
 Inputs: 2.
    - $crstype -- Container type: Course or Community
 
-   - $context -- Context: imsexport, examblock, dumpdocs, or shorturls
+   - $context -- Context: imsexport, examblock, dumpdocs, shorturls
+                          or passback.
 
 
 =item &clean()
@@ -219,13 +252,15 @@
 
 sub create_picker {
     my ($navmap,$context,$formname,$crstype,$blockedmaps,$blockedresources,$block,$preamble,
-        $numhome,$uploadedfiles,$tiny,$readonly) = @_;
+        $numhome,$uploadedfiles,$tiny,$passback,$readonly) = @_;
     return unless (ref($navmap));
     my ($it,$output,$numdisc,%discussiontime,%currmaps,%currresources,%files,
-        %shorturls,$chkname);
+        %shorturls,%shownmaps,%shownsymbs,%recursed,%retrieved,%pb,$chkname);
     $chkname = 'archive';
     if ($context eq 'shorturls') {
         $chkname = 'addtiny';
+    } elsif ($context eq 'passback') {
+        $chkname = 'passback';
     }
     $it = $navmap->getIterator(undef,undef,undef,1,undef,undef);
     if (ref($blockedmaps) eq 'HASH') {
@@ -237,6 +272,51 @@
         %files = %{$uploadedfiles};
     } elsif (ref($tiny) eq 'HASH') {
         %shorturls = %{$tiny}; 
+    } elsif ($context eq 'passback') {
+        if (ref($passback) eq 'HASH') {
+            %pb = %{$passback};
+            foreach my $symb (keys(%pb)) {
+                my ($map,$id,$url) = &Apache::lonnet::decode_symb($symb);
+                my @recurseup;
+                if ($url =~ /\.(page|sequence)$/) {
+                    @recurseup = $navmap->recurseup_maps($url);
+                    $shownmaps{&Apache::lonnet::clutter($url)} = 1;
+                    if (ref($pb{$symb}) eq 'HASH') {
+                        foreach my $entry (keys(%{$pb{$symb}})) {
+                            my $scope = (split("\0",$entry))[-1];
+                            if (($scope eq 'map') || ($scope eq 'rec')) {
+                                my @contents;
+                                if ($scope eq 'map') {
+                                    unless ($retrieved{$url} || $recursed{$url}) {
+                                        @contents = $navmap->retrieveResources($url,sub { $_[0]->is_gradable() },0);
+                                        $retrieved{$url} = 1;
+                                    }
+                                } elsif ($scope eq 'rec') {
+                                    unless ($recursed{$url}) {
+                                        @contents = $navmap->retrieveResources($url,sub { $_[0]->is_gradable() },1,0,1);
+                                        my @subfolders = $navmap->retrieveResources($url,sub { $_[0]->is_map() },1,0,1);
+                                        if (@subfolders) {
+                                            map { $shownmaps{$_->src()} = 1; } @subfolders;
+                                        }
+                                        $recursed{$url} = 1;
+                                    }
+                                }
+                                if (@contents) {
+                                    map { $shownsymbs{$_->symb()} = 1; } @contents;
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    @recurseup = $navmap->recurseup_maps($map);
+                    $shownmaps{&Apache::lonnet::clutter($map)} = 1;
+                    $shownsymbs{$symb} = 1;
+                }
+                if (@recurseup) {
+                    map { $shownmaps{&Apache::lonnet::clutter($_)} = 1; } @recurseup;
+                }
+            }
+        }
     }
     my @checked_maps;
     my $curRes;
@@ -256,7 +336,7 @@
     my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
     my $crsprefix = &propath($cdom,$cnum).'/userfiles/';
 
-    my ($info,$display,$onsubmit,$togglebuttons,$disabled);
+    my ($info,$display,$onsubmit,$togglebuttons,$disabled,$action);
     if ($context eq 'examblock') {
         my $maps_elem = 'docs_maps_'.$block;
         my $res_elem = 'docs_resources_'.$block;
@@ -279,16 +359,25 @@
     } elsif ($context eq 'imsexport') {
         $info = &mt('Choose which items you wish to export from your '.$crstype.'.');
         $startcount = 5;
+    } elsif ($context eq 'passback') {
+        $action = '/adm/grades';
+        $info = '<p>'.
+                &mt('Select link-protected launch item for which scores should be sent to launcher CMS, then push Next [_1].',
+                    '→').
+                '</p><br />';
+        if ($readonly) {
+            $disabled = ' disabled="disabled"';
+        }
     }
     if ($disabled) {
         $togglebuttons = '<br />';
-    } else {
+    } elsif ($context ne 'passback') {
         $togglebuttons = '<input type="button" value="'.&mt('check all').'" '.
                          'onclick="javascript:checkAll(document.'.$formname.'.'.$chkname.')" />'.
                          '  <input type="button" value="'.&mt('uncheck all').'"'.
                          ' onclick="javascript:uncheckAll(document.'.$formname.'.'.$chkname.')" />';
     }
-    $display = '<form name="'.$formname.'" action="" method="post"'.$onsubmit.'>'."\n";
+    $display = '<form name="'.$formname.'" action="'.$action.'" method="post"'.$onsubmit.'>'."\n";
     if ($context eq 'imsexport') {
         $display .= $info.
                     '<div class="LC_columnSection">'."\n".
@@ -309,7 +398,7 @@
                 '</fieldset>';
         }
         $display .= '</div>';
-    } elsif (($context eq 'examblock') || ($context eq 'shorturls')) {
+    } elsif (($context eq 'examblock') || ($context eq 'shorturls') || ($context eq 'passback')) {
         $display .= $info.$togglebuttons;
     } elsif ($context eq 'dumpdocs') {
         $display .= $preamble.
@@ -336,6 +425,12 @@
     } elsif ($context eq 'shorturls') {
         $display .= '<th colspan="2">'.&mt('Tiny URL').'</th>'.
                     '<th>'.&mt("Title in $crstype").'</th>';
+    } elsif ($context eq 'passback') {
+        $display .= '<th>'.&mt("Title in $crstype").'</th>'.
+                    '<th>'.&mt('Tiny URL Deep-link').'</th>'.
+                    '<th>'.&mt('Launcher').'</th>'.
+                    '<th  style="padding-left: 6px; padding-right: 6px">'.&mt('Score Type').'</th>'.
+                    '<th style="padding-left: 6px; padding-right: 6px">'.&mt('Select').'</th>';
     }
     $display .= &Apache::loncommon::end_data_table_header_row();
     while ($curRes = $it->next()) {
@@ -361,18 +456,26 @@
                 }
             }
             $count ++;
-            my ($currelem,$mapurl,$is_map);
+            my ($currelem,$mapurl,$is_map,$showitem);
             if ($context eq 'imsexport') {
                 $currelem = $count+$boards+$startcount;
             } else {
                 $currelem = $count+$startcount;
             }
-            $display .= &Apache::loncommon::start_data_table_row()."\n";
             if (($curRes->is_sequence()) || ($curRes->is_page())) {
                 $lastcontainer = $currelem;
                 $mapurl = (&Apache::lonnet::decode_symb($symb))[2];
                 $is_map = 1;
             }
+            if ($context eq 'passback') {
+                if (($curRes->is_sequence()) || ($curRes->is_page())) {
+                    next unless ($shownmaps{$curRes->src});
+                } else {
+                    next unless ($shownsymbs{$symb});
+                }
+            } else {
+                $display .= &Apache::loncommon::start_data_table_row()."\n";
+            }
             if ($context eq 'shorturls') {
                 if ($shorturls{$symb}) {
                     $display .= '<td> </td><td align="right"><b>'."/tiny/$cdom/$shorturls{$symb}".'</b></td>'."\n";
@@ -381,7 +484,7 @@
                                 'value="'.$count.'"'.$disabled.' />'.&mt('Add').'</label></td>'.
                                 '<td> </td>'."\n";
                 }
-            } else {
+            } elsif ($context ne 'passback') {
                 $display .= '<td><input type="checkbox" name="'.$chkname.'" value="'.$count.'" ';
                 if ($is_map) {
                     $display .= 'onclick="javascript:checkFolder(document.'.$formname.','."'$currelem'".')" ';
@@ -391,7 +494,7 @@
                     }
                 } else {
                     if ($curRes->is_problem()) {
-                       $numprobs ++;
+                        $numprobs ++;
                     }
                     $display .= 'onclick="javascript:checkResource(document.'.$formname.','."'$currelem'".')" ';
                     if ($currresources{$symb}) {
@@ -406,7 +509,7 @@
                 $display .= '<td valign="top">';
             }
             for (my $i=0; $i<$depth; $i++) {
-                $display .= "$whitespace\n";
+                $showitem .= "$whitespace\n";
             }
             my $icon = 'src="'.$location.'/unknown.gif" alt=""';
             if ($curRes->is_sequence()) {
@@ -420,7 +523,7 @@
             } elsif ($curRes->src ne '') {
                 $icon = 'src="'.&Apache::loncommon::icon($curRes->src).'" alt=""';
             }
-            $display .= '<img '.$icon.' /> '."\n";
+            $showitem .= '<img '.$icon.' /> '."\n";
             $children{$parent{$depth}} .= $currelem.':';
             if ($context eq 'examblock') {
                 if ($parent{$depth} > 1) {
@@ -431,8 +534,65 @@
                     }
                 }
             }
-            $display .= ' '.$curRes->title().$whitespace.'</td>'."\n";
-
+            $showitem .= ' '.$curRes->title().$whitespace;
+            if ($context eq 'passback') {
+                if ((exists($pb{$symb})) && (ref($pb{$symb}) eq 'HASH')) {
+                    my $numlinks = scalar(keys(%{$pb{$symb}}));
+                    my $count = 0;
+                    foreach my $launcher (sort(keys(%{$pb{$symb}}))) {
+                        if ($count == 0) {
+                            $display .= &Apache::loncommon::start_data_table_row()."\n";
+                            if ($numlinks > 1) {
+                                $display .= '<td rowspan="'.$numlinks.'">'.$showitem.'</td>';
+                            } else {
+                                $display .= '<td style="vertical-align: baseline">'.$showitem.'</td>';
+                            }
+                        } else {
+                            $display .= &Apache::loncommon::end_data_table_row().
+                                        &Apache::loncommon::start_data_table_row()."\n";
+                        }
+                        my ($linkuri,$linkprotector,$scope) = split("\0",$launcher);
+                        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)';
+                                }
+                            }
+                        }
+                        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');
+                        }
+                        $display .= '<td style="vertical-align: baseline"><span style="font-weight: bold;">'.$linkuri.'</span></td>'."\n".
+                                    '<td style="vertical-align: baseline; padding-left: 6px; padding-right: 6px">'.$appname.$setter.'</td>'."\n".
+                                    '<td style="vertical-align: baseline"><span style="font-style: italic;">'.$shownscope.'</span></td>'."\n".
+                                    '<td align="right" style="vertical-align: baseline"><input type="radio" name="'.$chkname.'" '.
+                                    'value="'.&escape($launcher).'"'.$disabled.' /></td>'."\n";
+                        $count ++;
+                    }
+                } else {
+                    $display .= &Apache::loncommon::start_data_table_row()."\n".
+                                '<td colspan="5">'.$showitem.'</td>';
+                }
+            } else {
+                $display .= $showitem.'</td>'."\n";
+            }
             if ($context eq 'imsexport') {
 # Existing discussion posts?
                 if ($discussiontime{$ressymb} > 0) {
@@ -519,8 +679,20 @@
                 '<input type="submit" name="shorturls" value="'.
                 &mt('Create Tiny URL(s)').'" /></p>';
         }
+    } elsif ($context eq 'passback') {
+        unless ($readonly) {
+            $display .=
+                '<p>'.
+                '<input type="hidden" name="symb" value="'.&Apache::lonenc::check_encrypt($env{'form.symb'}).'" />'."\n".
+                '<input type="hidden" name="command" value="passback" />'.
+                '<input type="submit" name="picklauncher" value="'.
+                &mt('Next').' →" /></p>';
+        }
     }
     $display .= '</form>';
+    if ($context eq 'passback') {
+        return $display;
+    }
     my $scripttag =
         &respicker_javascript($startcount,$numcount,$context,$formname,\%children,
                               \%hierarchy,\@checked_maps,$numhome,$chkname);
Index: doc/loncapafiles/loncapafiles.lpml
diff -u doc/loncapafiles/loncapafiles.lpml:1.1073 doc/loncapafiles/loncapafiles.lpml:1.1074
--- doc/loncapafiles/loncapafiles.lpml:1.1073	Fri Nov 15 16:25:30 2024
+++ doc/loncapafiles/loncapafiles.lpml	Tue Dec  3 23:34:12 2024
@@ -2,7 +2,7 @@
  "http://lpml.sourceforge.net/DTD/lpml.dtd">
 <!-- loncapafiles.lpml -->
 
-<!-- $Id: loncapafiles.lpml,v 1.1073 2024/11/15 16:25:30 raeburn Exp $ -->
+<!-- $Id: loncapafiles.lpml,v 1.1074 2024/12/03 23:34:12 raeburn Exp $ -->
 
 <!--
 
@@ -8625,6 +8625,7 @@
 navigation.png;
 network-workgroup.png;
 page.png;
+passback.png;
 preferences-desktop-font.png;
 preferences-desktop-locale.png;
 preferences-desktop-remote-desktop.png;


More information about the LON-CAPA-cvs mailing list