--- loncom/interface/loncourserespicker.pm 2024/11/24 04:17:50 1.18 +++ loncom/interface/loncourserespicker.pm 2024/12/20 12:25:13 1.20 @@ -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.20 2024/12/20 12:25:13 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -33,51 +33,73 @@ 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 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,27 +135,38 @@ Inputs: 11. - $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() 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, examblock, dumpdocs or shorturls). + (imsexport, examblock, dumpdocs, shorturls, or + crsauthored). - $formname -- Name of form. @@ -143,6 +176,12 @@ Inputs: 7. - $checked_maps -- Reference to array of folders currently checked. + - $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 tags. @@ -154,7 +193,8 @@ no object instantiated. 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 +259,15 @@ use LONCAPA qw(:DEFAULT :match); 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 +279,51 @@ sub create_picker { %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 +343,7 @@ sub create_picker { 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 +366,25 @@ sub create_picker { } 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 = '

'. + &mt('Select link-protected launch item for which scores should be sent to launcher CMS, then push Next [_1].', + '→'). + '


'; + if ($readonly) { + $disabled = ' disabled="disabled"'; + } } if ($disabled) { $togglebuttons = '
'; - } else { + } elsif ($context ne 'passback') { $togglebuttons = ''. '  '; } - $display = '
'."\n"; + $display = ''."\n"; if ($context eq 'imsexport') { $display .= $info. '
'."\n". @@ -309,7 +405,7 @@ sub create_picker { ''; } $display .= '
'; - } 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 +432,12 @@ sub create_picker { } elsif ($context eq 'shorturls') { $display .= ''.&mt('Tiny URL').''. ''.&mt("Title in $crstype").''; + } elsif ($context eq 'passback') { + $display .= ''.&mt("Title in $crstype").''. + ''.&mt('Tiny URL Deep-link').''. + ''.&mt('Launcher').''. + ''.&mt('Score Type').''. + ''.&mt('Select').''; } $display .= &Apache::loncommon::end_data_table_header_row(); while ($curRes = $it->next()) { @@ -361,18 +463,26 @@ sub create_picker { } } $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 .= ' '."/tiny/$cdom/$shorturls{$symb}".''."\n"; @@ -381,7 +491,7 @@ sub create_picker { 'value="'.$count.'"'.$disabled.' />'.&mt('Add').''. ' '."\n"; } - } else { + } elsif ($context ne 'passback') { $display .= 'is_problem()) { - $numprobs ++; + $numprobs ++; } $display .= 'onclick="javascript:checkResource(document.'.$formname.','."'$currelem'".')" '; if ($currresources{$symb}) { @@ -406,7 +516,7 @@ sub create_picker { $display .= ''; } 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 +530,7 @@ sub create_picker { } elsif ($curRes->src ne '') { $icon = 'src="'.&Apache::loncommon::icon($curRes->src).'" alt=""'; } - $display .= ' '."\n"; + $showitem .= ' '."\n"; $children{$parent{$depth}} .= $currelem.':'; if ($context eq 'examblock') { if ($parent{$depth} > 1) { @@ -431,8 +541,65 @@ sub create_picker { } } } - $display .= ' '.$curRes->title().$whitespace.''."\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 .= ''.$showitem.''; + } else { + $display .= ''.$showitem.''; + } + } 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 .= ''.$linkuri.''."\n". + ''.$appname.$setter.''."\n". + ''.$shownscope.''."\n". + ''."\n"; + $count ++; + } + } else { + $display .= &Apache::loncommon::start_data_table_row()."\n". + ''.$showitem.''; + } + } else { + $display .= $showitem.''."\n"; + } if ($context eq 'imsexport') { # Existing discussion posts? if ($discussiontime{$ressymb} > 0) { @@ -519,8 +686,20 @@ sub create_picker { '

'; } + } elsif ($context eq 'passback') { + unless ($readonly) { + $display .= + '

'. + ''."\n". + ''. + '

'; + } } $display .= '
'; + if ($context eq 'passback') { + return $display; + } my $scripttag = &respicker_javascript($startcount,$numcount,$context,$formname,\%children, \%hierarchy,\@checked_maps,$numhome,$chkname);