# The LearningOnline Network with CAPA # Routines to control the wishlist # # $Id: lonwishlist.pm,v 1.8 2010/08/25 12:38:45 wenzelju 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 Apache::lonwishlist - Wishlist-Module =head1 SYNOPSIS The wishlist offers a possibility to store links to resources from the resource-pool and external websites in a hierarchical list. It is only available for user with access to the resource-pool. The list can be structured by folders. The wishlist-module uses the CPAN-module "Tree" for easily handling the directory-structure of the wishlist. Each node in the tree has an index to be referenced by. =cut package Apache::lonwishlist; use strict; use Apache::Constants qw(:common); use Apache::lonnet; use Apache::loncommon(); use Apache::lonhtmlcommon; use Apache::lonlocal; use LONCAPA; use Tree; # Global variables my $root; my @childrenRt; my %TreeHash; my %TreeToHash; my @allFolders; my @allNodes; my $indentConst = 20; my $foldersOption; =pod =head2 Routines for getting and putting the wishlist data from and accordingly to users data. =over 4 =item * &getWishlist() Get the wishlist-data via lonnet::getkeys() and lonnet::get() and returns the got data in a hash. =item * &putWishlist(wishlist) Parameter is a reference to a hash. Puts the wishlist-data contained in the given hash via lonnet::put() to user-data. =item * &deleteWishlist() Deletes all entries from the user-data for wishlist. Do this before putting in new data. =back =cut # Read wishlist from user-data sub getWishlist { my @keys = &Apache::lonnet::getkeys('wishlist'); my %wishlist = &Apache::lonnet::get('wishlist',\@keys); foreach my $i ( keys %wishlist) { #File not found. This appears at the first time using the wishlist #Create file and put 'root' into it if ($i =~m/^error:No such file/) { &Apache::lonnet::logthis($i.'! Create file by putting in the "root" of the directory tree.'); &Apache::lonnet::put('wishlist', {'root' => ''}); my $options = ''; &Apache::lonnet::put('wishlist', {'folders' => $options}); @keys = &Apache::lonnet::getkeys('wishlist'); %wishlist = &Apache::lonnet::get('wishlist',\@keys); } elsif ($i =~ /^(con_lost|error|no_such_host)/i) { &Apache::lonnet::logthis('ERROR while attempting to get wishlist: '.$i); return 'error'; } } # if we got no keys in hash returned by get(), return error. # wishlist will not be loaded, instead the user will be asked to try again later if ((keys %wishlist) == 0) { &Apache::lonnet::logthis('ERROR while attempting to get wishlist: no keys retrieved!'); return 'error'; } return %wishlist; } # Write wishlist to user-data sub putWishlist { my $wishlist = shift; $foldersOption = ''; &getFoldersForOption(\@childrenRt); my $options = ''.$foldersOption; $foldersOption = ''; $$wishlist{'folders'} = $options; &Apache::lonnet::put('wishlist',$wishlist); } # Removes all existing entrys for wishlist in user-data sub deleteWishlist { my @wishlistkeys = &Apache::lonnet::getkeys('wishlist'); my %wishlist = &Apache::lonnet::del('wishlist',\@wishlistkeys); } =pod =head2 Routines for changing the directory struture of the wishlist. =over 4 =item * &newEntry(title, path, note) Creates a new entry in the wishlist containing the given informations. Additionally saves the date of creation in the entry. =item * &deleteEntries(marked) Parameter is a reference to an array containing the indices of all nodes that should be removed from the tree. =item * &sortEntries(indexNode, at) Changes the position of a node given by indexNode within its siblings. New position is given by at. =item * &moveEntries(indexNodesToMove, indexParent) Parameter is a reference to an array containing the indices of all nodes that should be moved. indexParent specifies the node that will become the new Parent for these nodes. =item * &setNewTitle(nodeindex, newTitle) Sets the title for the node given by nodeindex to newTitle. =item * &setNewPath(nodeindex, newPath) Sets the path for the node given by nodeindex to newPath. =item * &setNewNote(nodeindex, newNote) Sets the note for the node given by nodeindex to newNote. =item * &saveChanges() Prepares the wishlist-hash to save it via &putWishlist(wishlist). =back =cut # Create a new entry sub newEntry() { my ($title, $path, $note) = @_; my $date = gmtime(); # Create Entry-Object my $entry = Entry->new(title => $title, path => $path, note => $note, date => $date); # Create Tree-Object, this correspones a node in the wishlist-tree my $tree = Tree->new($entry); # Add this node to wishlist-tree my $folderIndex = $env{'form.folders'}; if ($folderIndex ne '') { @allFolders = (); &getFoldersToArray(\@childrenRt); my $folderToInsertOn = &Tree::getNodeByIndex($folderIndex,\@allFolders); $folderToInsertOn->add_child($tree); } else { $root->add_child($tree); } &saveChanges(); } # Delete entries sub deleteEntries { my $marked = shift; &getNodesToArray(\@childrenRt); foreach my $m (@$marked) { my $found = &Tree::getNodeByIndex($m, \@allNodes); &Tree::removeNode($found); } @allNodes = (); &saveChanges(); } # Sort entries sub sortEntries { my $indexNode = shift; my $at = shift; &getNodesToArray(\@childrenRt); my $foundNode = &Tree::getNodeByIndex($indexNode, \@allNodes); &Tree::moveNode($foundNode,$at,undef); @allNodes = (); } # Move entries sub moveEntries { my $indexNodesToMove = shift; my $indexParent = shift; my @nodesToMove = (); # get all nodes that should be moved &getNodesToArray(\@childrenRt); foreach my $index (@$indexNodesToMove) { my $foundNode = &Tree::getNodeByIndex($index, \@allNodes); push(@nodesToMove, $foundNode); } foreach my $node (@nodesToMove) { my $foundParent; my $parentIsIn = 0; foreach my $n (@nodesToMove) { if ($node->parent()->value() ne "root") { if ($node->parent()->value()->nindex() == $n->value()->nindex()) { $parentIsIn = 1; } } } if (!$parentIsIn) { if ($indexParent ne "root") { $foundParent = &Tree::getNodeByIndex($indexParent, \@allNodes); &Tree::moveNode($node,undef,$foundParent); } else { &Tree::moveNode($node,undef,$root); } } } @allNodes = (); } # Set a new title for an entry sub setNewTitle { my ($nodeindex, $newTitle) = @_; &getNodesToArray(\@childrenRt); my $found = &Tree::getNodeByIndex($nodeindex, \@allNodes); $found->value()->title($newTitle); @allNodes = (); } # Set a new path for an entry sub setNewPath { my ($nodeindex, $newPath) = @_; &getNodesToArray(\@childrenRt); my $found = &Tree::getNodeByIndex($nodeindex, \@allNodes); if ($found->value()->path()) { $found->value()->path($newPath); return 1; } @allNodes = (); return 0; } # Set a new note for an entry sub setNewNote { my ($nodeindex, $newNote) = @_; &getNodesToArray(\@childrenRt); my $found = &Tree::getNodeByIndex($nodeindex, \@allNodes); $found->value()->note($newNote); @allNodes = (); } # Save all changes sub saveChanges { @childrenRt = $root->children(); &Tree::TreeIndex(\@childrenRt); &Tree::setCountZero(); &Tree::RootToHash(\@childrenRt); &Tree::TreeToHash(\@childrenRt); &deleteWishlist(); &putWishlist(\%TreeToHash); } =pod =head2 Routines for handling the directory structure =over 4 =item * &getFoldersForOption(nodes) Return the titles for all exiting folders in an option-tag, used to offer the users a possibility to create a new link or folder in an existing folder. Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &getFoldersToArray(children) Puts all nodes that represent folders in the wishlist into an array. Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &getNodesToArray(children) Puts all existing nodes into an array (apart from the root node, because this one does not represent an entry in the wishlist). Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =back =cut # Return the names for all exiting folders in option-tags, so # a new link or a new folder can be created in an existing folder my $indent = 0; sub getFoldersForOption { my $nodes = shift; foreach my $n (@$nodes) { if ($n->value()->path() eq '') { $foldersOption .= ''; my @children = $n->children(); if ($#children >=0) { $indent += 10; &getFoldersForOption(\@children); $indent -= 10; } } } } # Put all folder-nodes to an array sub getFoldersToArray { my $children = shift; foreach my $c (@$children) { if ($c->value()->path() eq '') { push(@allFolders,$c); } my @newchildren = $c->children(); if ($#newchildren >= 0) { &getFoldersToArray(\@newchildren); } } } # Put all nodes to an array sub getNodesToArray { my $children = shift; foreach my $c (@$children) { push(@allNodes,$c); my @newchildren = $c->children(); if ($#newchildren >= 0) { &getNodesToArray(\@newchildren); } } } =pod =head2 Routines for the user-interface of the wishlist =over 4 =item * &JSforWishlist() Returns JavaScript-functions needed for wishlist actions like open and close folders. =item * &wishlistView(nodes) Returns the table-HTML-markup for the wishlist in mode "view". Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &wishlistEdit(nodes) Returns the table-HTML-markup for the wishlist in mode "edit". Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &wishlistMove(nodes, marked) Returns the table-HTML-markup for the wishlist in mode "move". Highlights all entry "selected to move" contained in marked (reference to array). Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &wishlistImport(nodes) Returns the table-HTML-markup for the wishlist in mode "import". Recursive call starting with all children of the root of the tree (parameter nodes is reference to an array containing the nodes of the current level). =item * &makePage(mode, marked) Returns the HTML-markup for the whole wishlist depending on mode. If mode is "move" we need the marked entries to be highlighted a "selected to move". Calls &wishlistView(nodes), &wishlistEdit(nodes) or &wishlistMove(nodes, marked). =item * &makePageSet() Returns the HTML-Markup for the page shown when a link was set by using the icon when viewing a resource. =item * &makePageImport() Returns the HTML-Markup for the page shown when links should be imported into courses. =item * &makeErrorPage () Returns the HTML-Markup for an error-page shown if the wishlist could not be loaded. =back =cut # Return a script-tag containing Javascript-function # needed for wishlist actions like 'new link' ect. sub JSforWishlist { my $startPagePopup = &Apache::loncommon::start_page('Wishlist',undef, {'only_body' => 1, 'js_ready' => 1, 'bgcolor' => '#FFFFFF',}); my $endPagePopup = &Apache::loncommon::end_page({'js_ready' => 1}); @allFolders = (); &getFoldersToArray(\@childrenRt); &getFoldersForOption(\@childrenRt); # texthash my %lt = &Apache::lonlocal::texthash( 'nl' => 'New Link', 'nf' => 'New Folder', 'lt' => 'Link Title', 'ft' => 'Folder Title', 'pa' => 'Path', 'nt' => 'Note', 'si' => 'Save in', 'cl' => 'Cancel'); my $inPageNewLink = '

'.$lt{'nl'}.'

'. '
'. &Apache::lonhtmlcommon::start_pick_box(). &Apache::lonhtmlcommon::row_title($lt{'lt'}). ''. &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title($lt{'pa'}). ''. &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title($lt{'nt'}). ''. &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::end_pick_box(). '

'. ''. ''. ''. '
'; my $inPageNewFolder = '

'.$lt{'nf'}.'

'. '
'. &Apache::lonhtmlcommon::start_pick_box(). &Apache::lonhtmlcommon::row_title($lt{'ft'}). '
'. &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title($lt{'nt'}). '
'. &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::end_pick_box(). '

'. ''. ''. ''. '
'; # Remove all \n for inserting on javascript document.write $inPageNewLink =~ s/\n//g; $inPageNewFolder =~ s/\n//g; # it is checked, wether a path links to a LON-CAPA-resource or an external website. links to course-contents are not allowed # because they probably will return a kind of 'no access' (unless the user is already in the course, the path links to). # also importing these kind of links into a course does not make much sense. # to find out if a path (not starting with /res/...) links to course-contents, the same filter as in lonwrapper is used, # that means that it is checked wether a path contains .problem, .quiz, .exam etc. # this is good for most cases but crashes as soon as a real external website contains one of this pattern in its URL. # so maybe there's a better way to find out wether a given URL belongs to a LON-CAPA-server or not ...? my $warningLinkNotAllowed1 = &mt('You can only insert links to LON-CAPA resources from the resource-pool '. 'or to external websites. Paths to LON-CAPA resources must be of the form /res/dom/usr... . '. 'Paths to external websites must contain the network protocol (e.g. http://...).'); my $warningLinkNotAllowed2 = &mt('The following link is not allowed: '); my $warningLink = &mt('You must insert a title and a path!'); my $warningFolder = &mt('You must insert a title!'); my $warningDelete = &mt('Are you sure you want to delete the selected entries? Deleting a folder also deletes all entries within this folder!'); my $warningSave = &mt('You have unsaved changes. You can either save these changes now by clicking "ok" or click "cancel" if you do not want to save your changes.'); my $warningMove = &mt('You must select a destination folder!'); $foldersOption = ''; my $js = &Apache::lonhtmlcommon::scripttag(<' +'function newlinksubmit(){' +'var path = document.getElementsByName("path")[0].value;' +'var title = document.getElementsByName("title")[0].value;' +'if (!path || !title) {' +'alert("$warningLink");' +'return false;}' +'var linkOK = (path.match(/^http:(\\\\/\\\\/)/) || path.match(/^https:(\\\\/\\\\/)/))' +'&& !(path.match(/\\.problem/) || path.match(/\\.exam/)' +'|| path.match(/\\.quiz/) || path.match(/\\.assess/)' +'|| path.match(/\\.survey/) || path.match(/\\.form/)' +'|| path.match(/\\.library/) || path.match(/\\.page/)' +'|| path.match(/\\.sequence/));' +'if (!path.match(/^(\\\\/res\\\\/)/) && !linkOK) {' +'alert("$warningLinkNotAllowed1");' +'return false;}' +'else {' +'window.close();' +'return true;}}' +'<\/scr'+'ipt>' +'$inPageNewLink' +'$endPagePopup'); newlinkWin.document.close(); } function newFolder() { newfolderWin=window.open('','newfolderWin','width=580,height=270, scrollbars=yes'); newfolderWin.document.write('$startPagePopup' +'