--- loncom/interface/loncourserespicker.pm	2013/01/15 17:39:58	1.5
+++ loncom/interface/loncourserespicker.pm	2024/12/26 17:12:36	1.21
@@ -1,6 +1,6 @@
 # The LearningOnline Network
 #
-# $Id: loncourserespicker.pm,v 1.5 2013/01/15 17:39:58 raeburn Exp $
+# $Id: loncourserespicker.pm,v 1.21 2024/12/26 17:12:36 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -33,53 +33,79 @@ loncourserespicker - Utilities to choose
 
 =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 duriation of an exam/quiz.  
+(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 Course Documents 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 
-content in which resource/folder selection is being made.
+context in which resource/folder selection is being made.
 
-The two contexts currently supported are: IMS export and 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.  
+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: 7.
+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). 
 
    - $context -- Context in which course resource selection is being made.
-                 Currently imsexport and examblock are supported.
+                 Currently imsexport, examblock, dumpdocs, and shorturls
+                 are supported.
 
    - $formname  -- Name of the form in the window from which the pop-up
                    used to select course items was launched. 
@@ -95,23 +121,52 @@ Inputs: 7.
    - $block  -- An internal ID (integer) used to track which exam
                 block currently being configured.
 
+   - $preamble -- HTML form elements used to select Authoring Space
+                  if more than one available, and also set name of 'Folder 
+                  in Authoring Space' where content will be dumped, when
+                  context is 'dumpdocs'.
+
+   - $numhome -- number of possible Authoring Spaces where content could
+                 be dumped when context is 'dumpdocs'.
+
+   - $uploadedfiles -- Reference to hash: keys are paths to files in
+                       /home/httpd/lonUsers/$cdom/$1/$2/$3/$cnum/userfiles.
+
+   - $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,
+                  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()
 
 Creates javascript functions for checking/unchecking all items, and
 for recursive checking triggered by checking a folder, or recursive
-(upeards) unchecking of an item within a folder. 
+(upwards) unchecking of an item within a folder. 
 
-Inputs: 7. 
+Inputs: 9.
    - $startcount -- Starting offset of form element numbering for items  
 
-   - $numcount -- Total numer of folders and resources in course.
+   - $numcount -- Total number of folders and resources in course.
 
    - $context -- Context in which resources are being displayed
-                 (imsexport or examblock). 
+                 (imsexport, examblock, dumpdocs, shorturls, or
+                  crsauthored). 
 
    - $formname --  Name of form.
 
@@ -121,7 +176,13 @@ Inputs: 7.
 
    - $checked_maps -- Reference to array of folders currently checked.
 
-Output: 1. Javascript (witthin <script></script> tags.
+   - $numhome -- Number of possible Authoring Spaces on this server 
+                 (context is dumpdocs or crsauthored).
+ 
+   - $chkname -- Name of checkboxes used to indicate selection of folder
+                 or resource.
+
+Output: 1. Javascript (within <script></script> tags.
 
 
 =item &get_navmap_object() 
@@ -132,7 +193,51 @@ no object instantiated.
 Inputs: 2.
    - $crstype -- Container type: Course or Community
 
-   - $context -- Context: imsexport or examblock
+   - $context -- Context: imsexport, examblock, dumpdocs, shorturls
+                          or passback.
+
+
+=item &clean()
+ 
+Takes incoming title and replaces non-alphanumeric characters with underscore,
+so title can be used as suggested file name (with appended extension) for file
+copied from course to Authoring Space.
+
+
+=item &enumerate_course_contents()
+
+Create hashes of maps (for folders/pages) and symbs (for resources) in
+a course, where keys are numbers (starting with 1) and values are
+map url, or symb, for an iteration through the course, as seen by
+a Course Coordinator. Used to generate numerical IDs to facilitate
+(a) storage of lists of maps or resources to be blocked during an exam,
+(b) processing selected form elements during dumping of selected course
+    content to Authoring Space.
+(c) processing of checked checkboxes for creation of shortened URLs for
+    deep-linking to course content.
+
+Inputs: 7 
+
+      $navmap - navmaps object
+
+      $map_url - reference to hash to contain URLs of maps in course
+
+      $resource_symb - reference to hash to contain symbs for
+                       resources in course
+
+      $title_ref - reference to hash containing titles for items in
+                   course
+
+      $context - examblock, dumpdocs or shorturls
+
+      $cdom - course's domain
+
+      $cnum - courseID
+
+Outputs: None
+
+Side Effects: $map_url and $resource_symb hashrefs are populated.
+
 
 =over
 
@@ -153,15 +258,72 @@ use Apache::lonlocal;
 use LONCAPA qw(:DEFAULT :match);
 
 sub create_picker {
-    my ($navmap,$context,$formname,$crstype,$blockedmaps,$blockedresources,$block) = @_;
+    my ($navmap,$context,$formname,$crstype,$blockedmaps,$blockedresources,$block,$preamble,
+        $numhome,$uploadedfiles,$tiny,$passback,$readonly) = @_;
     return unless (ref($navmap));
-    my ($it,$output,$numdisc,%maps,%resources,%discussiontime,%currmaps,%currresources);
+    my ($it,$output,$numdisc,%discussiontime,%currmaps,%currresources,%files,
+        %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') {
         %currmaps = %{$blockedmaps};
     }
     if (ref($blockedresources) eq 'HASH') {
         %currresources = %{$blockedresources};
+    } elsif (ref($uploadedfiles) eq 'HASH') {
+        %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;
@@ -174,42 +336,63 @@ sub create_picker {
     my %children = ();
     my %hierarchy = ();
     my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons");
-    my $whitespace = 
+    my $whitespace =
         '<img src="'.$location.'/whitespace_21.gif" class="LC_docs_spacer" alt="" />';
 
-    my $onsubmit;
+    my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+    my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+    my $crsprefix = &propath($cdom,$cnum).'/userfiles/';
+
+    my ($info,$display,$onsubmit,$togglebuttons,$disabled,$action);
     if ($context eq 'examblock') {
         my $maps_elem = 'docs_maps_'.$block;
         my $res_elem = 'docs_resources_'.$block;
         $onsubmit = ' onsubmit="return writeToOpener('."'$maps_elem','$res_elem'".');"';
+        $info = &mt('Items in '.lc($crstype).' for which access will be blocked.');
+        if ($readonly) {
+            $disabled = ' disabled="disabled"';
+        }
     }
-    my $display =
-       '<form name="'.$formname.'" action="" method="post"'.$onsubmit.'>'."\n".
-       '<p>';
-    if ($context eq 'imsexport') {
-        $display .= &mt('Choose which items you wish to export from your '.
-                        $crstype.'.');
+    if ($context eq 'dumpdocs') {
+        $info = '<span class="LC_fontsize_medium">'.
+                &mt('Choose the uploaded course items and templated pages/problems to be copied to Authoring Space.').
+                '</span><br /><br />';
+        $startcount = 3 + $numhome;
+        $onsubmit = ' onsubmit="return checkUnique(document.'.$formname.',document.'.$formname.'.'.$chkname.');"';
+    } elsif ($context eq 'shorturls') {
+        $info = '<span class="LC_fontsize_medium">'.
+                &mt('Choose the resource(s) and/or folder(s) from Main Content for which shortened URL(s) are needed.').
+                '</span><br /><br />';
+    } elsif ($context eq 'imsexport') {
+        $info = &mt('Choose which items you wish to export from your '.$crstype.'.');
         $startcount = 5;
-    } elsif ($context eq 'examblock') {
-        $display .= &mt('Items in '.lc($crstype).' for which access will be blocked.');
+    } 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].',
+                    '&rarr;').
+                '</p><br />';
+        if ($readonly) {
+            $disabled = ' disabled="disabled"';
+        }
     }
-    $display .= '</p>';
-    if ($context eq 'imsexport') {
-        $display .= '<div class="LC_columnSection">'."\n".
-                    '<fieldset>'.
-                    '<legend>'.&mt('Content items').'</legend>'."\n";
+    if ($disabled) {
+        $togglebuttons = '<br />';
+    } elsif ($context ne 'passback') {
+        $togglebuttons = '<input type="button" value="'.&mt('check all').'" '.
+                         'onclick="javascript:checkAll(document.'.$formname.'.'.$chkname.')" />'.
+                         '&nbsp;&nbsp;<input type="button" value="'.&mt('uncheck all').'"'.
+                         ' onclick="javascript:uncheckAll(document.'.$formname.'.'.$chkname.')" />';
     }
-    $display .= 
-        '<input type="button" value="'.&mt('check all').'" '.
-        'onclick="javascript:checkAll(document.'.$formname.'.archive)" />'.
-        '&nbsp;&nbsp;<input type="button" value="'.&mt('uncheck all').'"'.
-        ' onclick="javascript:uncheckAll(document.'.$formname.'.archive)" />';
+    $display = '<form name="'.$formname.'" action="'.$action.'" method="post"'.$onsubmit.'>'."\n";
     if ($context eq 'imsexport') {
-        $display .= '</fieldset>';
-        %discussiontime =
-            &Apache::lonnet::dump('discussiontimes',
-                                  $env{'course.'.$env{'request.course.id'}.'.domain'},
-                                  $env{'course.'.$env{'request.course.id'}.'.num'});
+        $display .= $info.
+                    '<div class="LC_columnSection">'."\n".
+                    '<fieldset>'.
+                    '<legend>'.&mt('Content items').'</legend>'."\n".
+                    $togglebuttons.
+                    '</fieldset>';
+        %discussiontime = &Apache::lonnet::dump('discussiontimes',$cdom,$cnum);
         $numdisc = keys(%discussiontime);
         if ($numdisc > 0) {
             $display .= 
@@ -221,7 +404,15 @@ sub create_picker {
                 ' onclick="javascript:uncheckAll(document.'.$formname.'.discussion)" />'.
                 '</fieldset>';
         }
-        $display .= '</div>';      
+        $display .= '</div>';
+    } elsif (($context eq 'examblock') || ($context eq 'shorturls') || ($context eq 'passback')) {
+        $display .= $info.$togglebuttons;
+    } elsif ($context eq 'dumpdocs') {
+        $display .= $preamble.
+                    '<div class="LC_left_float">'.
+                    '<fieldset>'.
+                    '<legend>'.&mt('Content to copy').('&nbsp;'x4).$togglebuttons.'</legend>'.
+                    $info;
     }
     my $lastcontainer = $startcount;
     $display .= &Apache::loncommon::start_data_table()
@@ -233,12 +424,23 @@ sub create_picker {
         }
     } elsif ($context eq 'examblock') {
         $display .= '<th>'.&mt('Access blocked?').'</th>';
+    } elsif ($context eq 'dumpdocs') {
+        $display .= '<th>'.&mt('Copy?').'</th>'.
+                    '<th>'.&mt("Title in $crstype").'</th>'.
+                    '<th>'.&mt('Internal Identifier').'</th>'.
+                    '<th>'.&mt('Save as ...').'</th>';
+    } 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()) {
-        if (ref($curRes)) {
-             $count ++;
-        }
         if ($curRes == $it->BEGIN_MAP()) {
             $depth++;
             $parent{$depth} = $lastcontainer;
@@ -250,35 +452,71 @@ sub create_picker {
         if (ref($curRes)) {
             my $symb = $curRes->symb();
             my $ressymb = $symb;
-            if ($ressymb =~ m|adm/($match_domain)/($match_username)/(\d+)/bulletinboard$|) {
+            if ($context eq 'dumpdocs') {
+                next unless (($curRes->src() =~ m{^\Q/uploaded/$cdom/$cnum/\E(docs|supplemental|simplepage)}) ||
+                             ($curRes->src() =~ m{^\Q/uploaded/$cdom/$cnum/\E(default|supplemental)_\d+\.(sequence|page)}) ||
+                             ($curRes->src() eq '/res/lib/templates/simpleproblem.problem') ||
+                             ($curRes->src() =~ m{^/adm/$match_domain/$match_username/\d+/smppg}));
+            } elsif ($ressymb =~ m|adm/($match_domain)/($match_username)/(\d+)/bulletinboard$|) {
                 unless ($ressymb =~ m|adm/wrapper/adm|) {
                     $ressymb = 'bulletin___'.$3.'___adm/wrapper/adm/'.$1.'/'.$2.'/'.$3.'/bulletinboard';
                 }
             }
-            my $currelem = $count+$boards+$startcount;
-            $display .= &Apache::loncommon::start_data_table_row().
-                       '<td>'."\n".
-                       '<input type="checkbox" name="archive" value="'.$count.'" ';
+            $count ++;
+            my ($currelem,$mapurl,$is_map,$showitem);
+            if ($context eq 'imsexport') {
+                $currelem = $count+$boards+$startcount;
+            } else {
+                $currelem = $count+$startcount;
+            }
             if (($curRes->is_sequence()) || ($curRes->is_page())) {
                 $lastcontainer = $currelem;
-                $display .= 'onclick="javascript:checkFolder(this.form,'."'$currelem'".')" ';
-                my $mapurl = (&Apache::lonnet::decode_symb($symb))[2];
-                if ($currmaps{$mapurl}) {
-                    $display .= 'checked="checked"';
-                    push(@checked_maps,$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 {
-                if ($curRes->is_problem()) {
-                    $numprobs ++;
+                $display .= &Apache::loncommon::start_data_table_row()."\n";
+            }
+            if ($context eq 'shorturls') {
+                if ($shorturls{$symb}) {
+                    $display .= '<td>&nbsp;</td><td align="right"><b>'."/tiny/$cdom/$shorturls{$symb}".'</b></td>'."\n";
+                } else {
+                    $display .= '<td align="left"><label><input type="checkbox" name="'.$chkname.'" '.
+                                'value="'.$count.'"'.$disabled.' />'.&mt('Add').'</label></td>'.
+                                '<td>&nbsp;</td>'."\n";
                 }
-                $display .= 'onclick="javascript:checkResource(this.form,'."'$currelem'".')" ';
-                if ($currresources{$symb}) {
-                    $display .= 'checked="checked"';
+            } elsif ($context ne 'passback') {
+                $display .= '<td><input type="checkbox" name="'.$chkname.'" value="'.$count.'" ';
+                if ($is_map) {
+                    $display .= 'onclick="javascript:checkFolder(document.'.$formname.','."'$currelem'".')" ';
+                    if ($currmaps{$mapurl}) {
+                        $display .= 'checked="checked"';
+                        push(@checked_maps,$currelem);
+                    }
+                } else {
+                    if ($curRes->is_problem()) {
+                        $numprobs ++;
+                    }
+                    $display .= 'onclick="javascript:checkResource(document.'.$formname.','."'$currelem'".')" ';
+                    if ($currresources{$symb}) {
+                        $display .= 'checked="checked"';
+                    }
                 }
+                $display .= $disabled.' />'."\n";
+            }
+            if ($context eq 'dumpdocs') {
+                $display .= '</td><td valign="top">';
+            } elsif ($context eq 'shorturls') {
+                $display .= '<td valign="top">';
             }
-            $display .= ' />'."\n";
             for (my $i=0; $i<$depth; $i++) {
-                $display .= "$whitespace\n";
+                $showitem .= "$whitespace\n";
             }
             my $icon = 'src="'.$location.'/unknown.gif" alt=""';
             if ($curRes->is_sequence()) {
@@ -292,7 +530,7 @@ sub create_picker {
             } elsif ($curRes->src ne '') {
                 $icon = 'src="'.&Apache::loncommon::icon($curRes->src).'" alt=""';
             }
-            $display .= '<img '.$icon.' />&nbsp;'."\n";
+            $showitem .= '<img '.$icon.' />&nbsp;'."\n";
             $children{$parent{$depth}} .= $currelem.':';
             if ($context eq 'examblock') {
                 if ($parent{$depth} > 1) {
@@ -303,8 +541,65 @@ sub create_picker {
                     }
                 }
             }
-            $display .= '&nbsp;'.$curRes->title().'</td>'."\n";
-
+            $showitem .= '&nbsp;'.$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) {
@@ -315,6 +610,35 @@ sub create_picker {
                 } elsif ($numdisc > 0) {
                     $display .= '<td>&nbsp;</td>'."\n";
                 }
+            } elsif ($context eq 'dumpdocs') {
+                my $src = $curRes->src();
+                my ($filepath,$title);
+                if ($src =~ m{^\Q/uploaded/$cdom/$cnum/\E}) {
+                    $filepath = &Apache::lonnet::filelocation('',$src);
+                    $filepath =~ s/\Q$crsprefix\E//;
+                    if ($curRes->is_map()) {
+                        $title = $files{$filepath};
+                    } else {
+                        $filepath =~ s{docs/}{}; 
+                        $title = $filepath;
+                        $title =~ s{^(default|\d+)/\d*/?}{};
+                    }
+                } else {
+                    $title = $curRes->title();
+                    $title =~ s{/}{_}g;
+                    $title = &clean($title);
+                    if ($src eq '/res/lib/templates/simpleproblem.problem') {
+                        my ($map,$id,$res) = &Apache::lonnet::decode_symb($symb);
+                        $map =~ s{^uploaded/$cdom/$cnum/}{};
+                        $filepath = $map.'_'.$id;
+                        $title .= '.problem';
+                    } elsif ($src =~ m{^/adm/$match_domain/$match_username/(\d+)/smppg}) {
+                        $filepath = 'smppage_'.$1.'.db';
+                        $title .= '.html';
+                    }
+                }
+                $display .= '<td>'.$filepath.'</td>'.
+                            '<td><input type="text" size="40" name="namefor_'.$count.'" id="namefor_'.$count.'" value="'.$title.'" /></td>'."\n";
             }
             $display .= &Apache::loncommon::end_data_table_row();
         }
@@ -332,24 +656,58 @@ sub create_picker {
                         '&nbsp;'.&mt('Text').'</label></span></p>';
         }
     }
-    $display .= '<p>';
+    my $numcount;
     if ($context eq 'imsexport') {
         $display .= 
+           '<p>'.
            '<input type="hidden" name="finishexport" value="1" />'.
            '<input type="submit" name="exportcourse" value="'.
-           &mt('Export').'" />';
+           &mt('Export').'" /></p>';
+        $numcount = $count + $boards + $startcount;
     } elsif ($context eq 'examblock') {
-        $display .=
-            '<input type="submit" name="resourceblocks" value="'.
-            &mt('Copy Choices to Main Window').'" />';
-    }
-    $display .= '</p></form>';
-    my $numcount = $count + $boards + $startcount;
-    my $scripttag = 
+        unless ($readonly) {
+            $display .=
+                '<p>'.
+                '<input type="submit" name="resourceblocks" value="'.
+                &mt('Copy Choices to Main Window').'" /></p>';
+        }
+        $numcount = $count + $startcount;
+    } elsif ($context eq 'dumpdocs') {
+        $display .= '</fieldset>'.
+                    '</div><div style="padding:0;clear:both;margin:0;border:0"></div>'.
+                    '<div>'.
+                    '<input type="submit" name="dumpcourse" value="'.&mt("Copy $crstype Content").'" />'.
+                    '</div>';
+        $numcount = $count + $startcount;
+    } elsif ($context eq 'shorturls') {
+        unless ($readonly) {
+            $display .=
+                '<p>'.
+                '<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').' &rarr;" /></p>';
+        }
+    }
+    $display .= '</form>';
+    if ($context eq 'passback') {
+        return $display;
+    }
+    my $scripttag =
         &respicker_javascript($startcount,$numcount,$context,$formname,\%children,
-                              \%hierarchy,\@checked_maps);
+                              \%hierarchy,\@checked_maps,$numhome,$chkname);
+    if (($context eq 'dumpdocs') || ($context eq 'shorturls')) {
+        return $scripttag.$display; 
+    }
     my ($title,$crumbs,$args);
-    if ($context eq 'imsexport') {
+    if ($context eq 'imsexport') { 
         $title = 'Export '.$crstype.' to IMS Package';
     } elsif ($context eq 'examblock') {
         $title = 'Resources with Access blocked';
@@ -361,6 +719,9 @@ sub create_picker {
     if ($context eq 'imsexport') {
         $output .= &Apache::lonhtmlcommon::breadcrumbs('IMS Export').
                    &Apache::londocs::startContentScreen('tools');
+    } elsif ($context eq 'dumpdocs') {
+        $output .= &Apache::lonhtmlcommon::breadcrumbs('Copying to Authoring Space').
+                   &Apache::londocs::startContentScreen('tools');
     }
     $output .= $display;
     if ($context eq 'examblock') {
@@ -373,12 +734,8 @@ sub create_picker {
 
 sub respicker_javascript {
     my ($startcount,$numitems,$context,$formname,$children,$hierarchy,
-        $checked_maps) = @_;
-    return unless ((ref($children) eq 'HASH') && (ref($hierarchy) eq 'HASH')
-                   && (ref($checked_maps) eq 'ARRAY'));
-    my $scripttag = <<"START";
-<script type="text/javascript">
-// <![CDATA[
+        $checked_maps,$numhome,$chkname) = @_;
+    my $check_uncheck = <<"FIRST";
 function checkAll(field) {
     if (field.length > 0) {
         for (i = 0; i < field.length; i++) {
@@ -398,9 +755,35 @@ function uncheckAll(field) {
         field.checked = false;
     }
 }
+FIRST
+    if ($context eq 'shorturls') {
+        return <<"END";
+<script type="text/javascript">
+// <![CDATA[
+$check_uncheck
+// ]]>
+</script>
+END
+    }
+    return unless ((ref($children) eq 'HASH') && (ref($hierarchy) eq 'HASH')
+                   && (ref($checked_maps) eq 'ARRAY'));
+    my ($elem,$nested,$nameforelem);
+    if ($context eq 'dumpdocs') {
+        $elem='((parseInt(item)-'.$startcount.')*2)+'.$startcount;
+        $nested='((parseInt(nesting[item][i])-'.$startcount.')*2)+'.$startcount;
+        $nameforelem=$elem+1;
+    } else {
+        $elem='parseInt(item)';
+        $nested='parseInt(nesting[item][i])';
+    }
+    my $scripttag = <<"START";
+<script type="text/javascript">
+// <![CDATA[
+$check_uncheck
 
 function checkFolder(form,item) {
-    if (form.elements[item].checked == true) {
+    var elem = $elem;
+    if (form.elements[elem].checked == true) {
         containerCheck(form,item);
     } else {
         containerUncheck(form,item);
@@ -408,12 +791,13 @@ function checkFolder(form,item) {
 }
 
 function checkResource(form,item) {
-    if (form.elements[item].checked == false) {
+    var elem = $elem;
+    if (form.elements[elem].checked == false) {
         containerUncheck(form,item);
     }
 }
 
-numitems = $numitems;
+numitems = $startcount + $numitems;
 var parents = new Array(numitems);
 var nesting = new Array(numitems);
 var initial = new Array();
@@ -452,12 +836,76 @@ function recurseFolders() {
 }
 
 EXTRA
+    } elsif ($context eq 'dumpdocs') {
+        my $blankmsg = &mt('An item selected has no filename set in the "Save as ..." column.');
+        my $dupmsg = &mt('Items selected for copying need unique filenames in the "Save as ..." column.');
+        my $homemsg = &mt('An Authoring Space needs to be selected.');
+        &js_escape(\$blankmsg);
+        &js_escape(\$dupmsg);
+        &js_escape(\$homemsg);
+        $scripttag .= <<"EXTRA";
+
+function checkUnique(form,field) {
+    var duplicate = 0;
+    var blank = 0;
+    var numhome = '$numhome';
+    if (field.length > 0) {
+        for (i=0; i<field.length; i++) {
+            if (field[i].checked) {
+                var item = field[i].value;
+                var namefor = document.getElementById('namefor_'+item);
+                if (namefor) {
+                    var possval = namefor.value;
+                    if (!possval) {
+                        blank = item;
+                        break;
+                    }
+                    for (j=i+1; j<field.length; j++) {
+                        if (field[j].checked) {
+                            var curritem = field[j].value;
+                            var currnamefor = document.getElementById('namefor_'+curritem);
+                            if (currnamefor) {
+                                var currval = currnamefor.value;
+                                if (currval == possval) {
+                                    duplicate = curritem;
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    if (duplicate) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+    if (blank) {
+        alert('$blankmsg');
+        return false;
+    }
+    if (duplicate) {
+        alert('$dupmsg');
+        return false;
+    }
+    if (numhome > 1) {
+        if (!form.authorspace.options[form.authorspace.selectedIndex].value) {
+            alert('$homemsg');
+            return false;
+        }
+    }
+    return true;
+}
+
+EXTRA
+
     }
 
     $scripttag .= <<"END";
 
 function containerCheck(form,item) {
-    form.elements[item].checked = true;
+    var elem = $elem;
+    form.elements[elem].checked = true;
     if(Object.prototype.toString.call(parents[item]) === '[object Array]') {
         if (parents[item].length > 0) {
             for (var j=0; j<parents[item].length; j++) {
@@ -471,7 +919,7 @@ function containerUncheck(form,item) {
     if(Object.prototype.toString.call(nesting[item]) === '[object Array]') {
         if (nesting[item].length > 0) {
             for (var i=0; i<nesting[item].length; i++) {
-                form.elements[nesting[item][i]].checked = false;
+                var nested = $nested;
             }
         }
     }
@@ -485,8 +933,8 @@ END
 function writeToOpener(maps,resources) {
     var checkedmaps = '';
     var checkedresources = '';
-    for (var i=0; i<document.$formname.archive.length; i++) {
-        if (document.$formname.archive[i].checked) {
+    for (var i=0; i<document.$formname.${chkname}.length; i++) {
+        if (document.$formname.${chkname}[i].checked) {
             var isResource = 1;
             var include = 1;
             var elemnum = i+1+$startcount;
@@ -496,18 +944,20 @@ function writeToOpener(maps,resources) {
                 }
             }
             if (isResource == 1) {
-                if (nesting[elemnum].length > 0) {
-                    var lastelem = nesting[elemnum].length-1;
-                    if (document.$formname.elements[nesting[elemnum][lastelem]].checked) {
-                        include = 0;
+                if (nesting[elemnum] != null) {
+                    if (nesting[elemnum].length > 0) {
+                        var lastelem = nesting[elemnum].length-1;
+                        if (document.$formname.elements[nesting[elemnum][lastelem]].checked) {
+                            include = 0;
+                        }
                     }
                 }
             }
             if (include == 1) {
                 if (isResource == 1) {
-                    checkedresources += document.$formname.archive[i].value+',';
+                    checkedresources += document.$formname.${chkname}[i].value+',';
                 } else {
-                    checkedmaps += document.$formname.archive[i].value+',';
+                    checkedmaps += document.$formname.${chkname}[i].value+',';
                 }
             }
         }
@@ -540,7 +990,11 @@ sub get_navmap_object {
             $outcome = &Apache::loncommon::start_page('Selection of Resources for Blocking',
                                                        undef,{'only_body' => 1,}).
                       '<h2>'.&mt('Resource Display Failed').'</h2>';  
-        } 
+        } elsif ($context eq 'dumpdocs') {
+            $outcome = '<h2>'.&mt('Copying to Authoring Space unavailable').'</h2>';
+        } elsif ($context eq 'shorturls') {
+            $outcome = '<h2>'.&mt('Display/Setting of shortened URLs unavailable').'</h2>';
+        }
         $outcome .= '<div class="LC_error">';
         if ($crstype eq 'Community') {
             $outcome .= &mt('Unable to retrieve information about community contents');
@@ -548,7 +1002,7 @@ sub get_navmap_object {
             $outcome .= &mt('Unable to retrieve information about course contents');
         }
         $outcome .= '</div>';
-        if ($context eq 'imsexport') {
+        if (($context eq 'imsexport') || ($context eq 'dumpdocs') || ($context eq 'shorturls') ) {
             $outcome .= '<a href="/adm/coursedocs">';
             if ($crstype eq 'Community') {
                 $outcome .= &mt('Return to Community Editor');
@@ -556,7 +1010,13 @@ sub get_navmap_object {
                 $outcome .= &mt('Return to Course Editor');
             }
             $outcome .= '</a>';
-            &Apache::lonnet::logthis('IMS export failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'});
+            if ($context eq 'imsexport') {
+                &Apache::lonnet::logthis('IMS export failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'});
+            } elsif ($context eq 'dumpdocs') {
+                &Apache::lonnet::logthis('Copying to Authoring Space failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'});
+            } elsif ($context eq 'shorturls') {
+                &Apache::lonnet::logthis('Displaying and/or saving URL shortcuts failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'});
+            }
         } elsif ($context eq 'examblock') {
             $outcome .=  '<href="javascript:window.close();">'.&mt('Close window').'</a>';         
         }
@@ -566,4 +1026,48 @@ sub get_navmap_object {
     }
 }
 
+sub clean {
+    my ($title)=@_;
+    $title=~s/[^\w\/\!\$\%\^\*\-\_\=\+\;\:\,\\\|\`\~]+/\_/gs;
+    return $title;
+}
+
+sub enumerate_course_contents {
+    my ($navmap,$map_url,$resource_symb,$titleref,$context,$cdom,$cnum) = @_;
+    if ((ref($navmap)) && (ref($map_url) eq 'HASH') &&
+        (ref($resource_symb) eq 'HASH') && (ref($titleref) eq 'HASH')) {
+        my $it = $navmap->getIterator(undef,undef,undef,1,undef,undef);
+        my $count = 0;
+        while (my $curRes = $it->next()) {
+            if (ref($curRes)) {
+                my $symb = $curRes->symb();
+                my $ressymb = $symb;
+                if ($context eq 'dumpdocs') {
+                    next unless (($curRes->src() =~ m{^\Q/uploaded/$cdom/$cnum/\E(docs|supplemental|simplepage)/}) ||
+                                 ($curRes->src() =~ m{^\Q/uploaded/$cdom/$cnum/\E(default|supplemental)_\d+\.(sequence|page)}) ||
+                                 ($curRes->src() eq '/res/lib/templates/simpleproblem.problem') ||
+                                 ($curRes->src() =~ m{^/adm/$match_domain/$match_username/\d+/smppg}));
+                } elsif ($ressymb =~ m{adm/($match_domain)/($match_username)/(\d+)/bulletinboard$}) {
+                    unless ($ressymb =~ m{adm/wrapper/adm}) {
+                        $ressymb = 'bulletin___'.$3.'___adm/wrapper/adm/'.$1.'/'.$2.'/'.$3.
+                                   '/bulletinboard';
+                    }
+                }
+                $count ++;
+                if ($context eq 'shorturls') {
+                    $resource_symb->{$count} = $ressymb;
+                } else {
+                    if (($curRes->is_sequence()) || ($curRes->is_page())) {
+                        $map_url->{$count} = (&Apache::lonnet::decode_symb($symb))[2];
+                    } else {
+                        $resource_symb->{$count} = $ressymb;
+                    }
+                }
+                $titleref->{$count} = $curRes->title();
+            }
+        }
+    }
+    return;
+}
+
 1;