# The LearningOnline Network # # $Id: loncourserespicker.pm,v 1.12 2015/04/09 17:57:05 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # =pod =head1 NAME loncourserespicker - Utilities to choose folders and resources in a course. =head1 SYNOPSIS loncourserespicker provides an interface for selecting which folders and/or resources are to be either: (a) exported to an IMS Content Package (b) subject to access blocking for the duriation of an exam/quiz. (c) dumped to an Authoring Space =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. =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. There is a submit button, which will be named differently according to the context in which resource/folder selection is being made. The three contexts currently supported are: IMS export, selection of content to be subject to access restructions for the duration of an exam, and selection of items for dumping to an Authoring Space. =head1 INTERNAL SUBROUTINES =item &create_picker() Created HTML mark up 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: 9. - $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. - $formname -- Name of the form in the window from which the pop-up used to select course items was launched. - $crstype -- Course or Community - $blockedmaps -- Reference to hash of previously selected maps (e.g., for a live exam block). - $blockedresources -- Reference to hash of resources selected previously (e.g., for an exam block). - $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. Output: $output is the HTML mark-up for display/selection of content items in the pop-up window. =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. Inputs: 7. - $startcount -- Starting offset of form element numbering for items - $numcount -- Total numer of folders and resources in course. - $context -- Context in which resources are being displayed (imsexport, examblock or dumpdocs). - $formname -- Name of form. - $children -- Reference to hash of items contained within a folder. - $hierarchy -- Reference to hierarchy of folders containing an item. - $checked_maps -- Reference to array of folders currently checked. Output: 1. Javascript (within tags. =item &get_navmap_object() Instantiates a navmaps object, and generates an error message if no object instantiated. Inputs: 2. - $crstype -- Container type: Course or Community - $context -- Context: imsexport, examblock or dumpdocs =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 element during dumping of selected course content to Authoring Space. 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 or dumpdocs $cdom - course's domain $cnum - courseID Outputs: None Side Effects: $map_url and $resource_symb hashrefs are populated. =over =back =cut package Apache::loncourserespicker; use strict; use Apache::lonnet; use Apache::loncommon; use Apache::lonhtmlcommon; use Apache::lonnavmaps; use Apache::londocs; use Apache::lonlocal; use LONCAPA qw(:DEFAULT :match); sub create_picker { my ($navmap,$context,$formname,$crstype,$blockedmaps,$blockedresources,$block,$preamble,$numhome,$uploadedfiles) = @_; return unless (ref($navmap)); my ($it,$output,$numdisc,%maps,%resources,%discussiontime,%currmaps,%currresources,%files); $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}; } my @checked_maps; my $curRes; my $numprobs = 0; my $depth = 0; my $count = 0; my $boards = 0; my $startcount = 1; my %parent = (); my %children = (); my %hierarchy = (); my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); my $whitespace = ''; 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); 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 ($context eq 'dumpdocs') { $info = ''. &mt('Choose the uploaded course items and templated pages/problems to be copied to Authoring Space.'). '

'; $startcount = 3 + $numhome; $onsubmit = ' onsubmit="return checkUnique(document.'.$formname.',document.'.$formname.'.archive);"'; } elsif ($context eq 'imsexport') { $info = &mt('Choose which items you wish to export from your '.$crstype.'.'); $startcount = 5; } $togglebuttons = ''. '  '; $display = '
'."\n"; if ($context eq 'imsexport') { $display .= $info. '
'."\n". '
'. ''.&mt('Content items').''."\n". $togglebuttons. '
'; %discussiontime = &Apache::lonnet::dump('discussiontimes',$cdom,$cnum); $numdisc = keys(%discussiontime); if ($numdisc > 0) { $display .= '
'. ''.&mt('Discussion posts').''. ''. '  '. '
'; } $display .= '
'; } elsif ($context eq 'examblock') { $display .= $info.$togglebuttons; } elsif ($context eq 'dumpdocs') { $display .= $preamble. '
'. '
'. ''.&mt('Content to copy').(' 'x4).$togglebuttons.''. $info; } my $lastcontainer = $startcount; $display .= &Apache::loncommon::start_data_table() .&Apache::loncommon::start_data_table_header_row(); if ($context eq 'imsexport') { $display .= ''.&mt('Export content item?').''; if ($numdisc > 0) { $display .= ''.&mt('Export discussion posts?').''; } } elsif ($context eq 'examblock') { $display .= ''.&mt('Access blocked?').''; } elsif ($context eq 'dumpdocs') { $display .= ''.&mt('Copy?').''. ''.&mt("Title in $crstype"). ''.&mt('Internal Identifier').''. ''.&mt('Save as ...').''; } $display .= &Apache::loncommon::end_data_table_header_row(); while ($curRes = $it->next()) { if ($curRes == $it->BEGIN_MAP()) { $depth++; $parent{$depth} = $lastcontainer; } if ($curRes == $it->END_MAP()) { $depth--; $lastcontainer = $parent{$depth}; } 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 ++; my $currelem; if ($context eq 'imsexport') { $currelem = $count+$boards+$startcount; } else { $currelem = $count+$startcount; } $display .= &Apache::loncommon::start_data_table_row(). ''."\n". 'is_sequence()) || ($curRes->is_page())) { $lastcontainer = $currelem; $display .= 'onclick="javascript:checkFolder(document.'.$formname.','."'$currelem'".')" '; my $mapurl = (&Apache::lonnet::decode_symb($symb))[2]; 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 .= ' />'."\n"; if ($context eq 'dumpdocs') { $display .= ''; } for (my $i=0; $i<$depth; $i++) { $display .= "$whitespace\n"; } my $icon = 'src="'.$location.'/unknown.gif" alt=""'; if ($curRes->is_sequence()) { $icon = 'src="'.$location.'/navmap.folder.open.gif" alt="'.&mt('Folder').'"'; } elsif ($curRes->is_page()) { $icon = 'src="'.$location.'/navmap.page.open.gif" alt="'.&mt('Composite Page').'"'; } elsif ($curRes->is_problem()) { $icon = 'src="'.$location.'/problem.gif" alt="'.&mt('Problem').'"'; } elsif ($curRes->is_task()) { $icon = 'src="'.$location.'/task.gif" alt="'.&mt('Task').'"'; } elsif ($curRes->src ne '') { $icon = 'src="'.&Apache::loncommon::icon($curRes->src).'" alt=""'; } $display .= ' '."\n"; $children{$parent{$depth}} .= $currelem.':'; if ($context eq 'examblock') { if ($parent{$depth} > 1) { if ($hierarchy{$parent{$depth}}) { $hierarchy{$currelem} = $hierarchy{$parent{$depth}}.",'$parent{$depth}'"; } else { $hierarchy{$currelem} = "'$parent{$depth}'"; } } } $display .= ' '.$curRes->title().$whitespace.''."\n"; if ($context eq 'imsexport') { # Existing discussion posts? if ($discussiontime{$ressymb} > 0) { $boards ++; $display .= '' .'' .''."\n"; } elsif ($numdisc > 0) { $display .= ' '."\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 .= ''.$filepath.''. ''."\n"; } $display .= &Apache::loncommon::end_data_table_row(); } } $display .= &Apache::loncommon::end_data_table(); if ($context eq 'imsexport') { if ($numprobs > 0) { $display .= '

'. &mt('Export format for LON-CAPA problems:'). ''.(' ' x3). ''.(' ' x3). '

'; } } my $numcount; if ($context eq 'imsexport') { $display .= '

'. ''. '

'; $numcount = $count + $boards + $startcount; } elsif ($context eq 'examblock') { $display .= '

'. '

'; $numcount = $count + $startcount; } elsif ($context eq 'dumpdocs') { $display .= '
'. '
'. '
'. ''. '
'; $numcount = $count + $startcount; } $display .= '
'; my $scripttag = &respicker_javascript($startcount,$numcount,$context,$formname,\%children, \%hierarchy,\@checked_maps,$numhome); if ($context eq 'dumpdocs') { return $scripttag.$display; } my ($title,$crumbs,$args); if ($context eq 'imsexport') { $title = 'Export '.$crstype.' to IMS Package'; } elsif ($context eq 'examblock') { $title = 'Resources with Access blocked'; $args = {'only_body' => 1, 'add_entries' => { onload => 'javascript:recurseFolders();' }, }; } $output = &Apache::loncommon::start_page($title,$scripttag,$args); 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') { $output .= &Apache::loncommon::end_page(); } elsif ($context eq 'imsexport') { $output .= &Apache::londocs::endContentScreen(); } return $output; } sub respicker_javascript { my ($startcount,$numitems,$context,$formname,$children,$hierarchy, $checked_maps,$numhome) = @_; 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"; '; return $scripttag; } sub get_navmap_object { my ($crstype,$context) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); my $outcome; if (!defined($navmap)) { if ($context eq 'imsexport') { $outcome = &Apache::loncommon::start_page('Export '.$crstype.' to IMS Package'). '

'.&mt('IMS Export Failed').'

'; } elsif ($context eq 'examblock') { $outcome = &Apache::loncommon::start_page('Selection of Resources for Blocking', undef,{'only_body' => 1,}). '

'.&mt('Resource Display Failed').'

'; } elsif ($context eq 'dumpdocs') { $outcome = '

'.&mt('Copying to Authoring Space unavailable'); } $outcome .= '
'; if ($crstype eq 'Community') { $outcome .= &mt('Unable to retrieve information about community contents'); } else { $outcome .= &mt('Unable to retrieve information about course contents'); } $outcome .= '
'; if (($context eq 'imsexport') || ($context eq 'dumpdocs')) { $outcome .= ''; if ($crstype eq 'Community') { $outcome .= &mt('Return to Community Editor'); } else { $outcome .= &mt('Return to Course Editor'); } $outcome .= ''; if ($context eq 'imsexport') { &Apache::lonnet::logthis('IMS export failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'}); } else { &Apache::lonnet::logthis('Copying to Authoring Space failed - could not create navmap object in '.lc($crstype).':'.$env{'request.course.id'}); } } elsif ($context eq 'examblock') { $outcome .= ''.&mt('Close window').''; } return (undef,$outcome); } else { return ($navmap); } } 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 (($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;