--- loncom/interface/lonwishlist.pm 2010/08/10 14:30:20 1.1 +++ loncom/interface/lonwishlist.pm 2014/01/29 16:25:44 1.19 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA -# Routines to control the wishlist +# Utility-routines for wishlist # -# $Id: lonwishlist.pm,v 1.1 2010/08/10 14:30:20 wenzelju Exp $ +# $Id: lonwishlist.pm,v 1.19 2014/01/29 16:25:44 bisitz Exp $ # # Copyright Michigan State University Board of Trustees # @@ -26,16 +26,31 @@ # 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. + +=back + +=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 qw(:DEFAULT :match); use Tree; @@ -47,20 +62,48 @@ 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 @wishlistkeys = &Apache::lonnet::getkeys('wishlist'); - my %wishlist = &Apache::lonnet::get('wishlist',\@wishlistkeys); - foreach my $i (%wishlist) { + 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' => ''}); - @wishlistkeys = &Apache::lonnet::getkeys('wishlist'); - %wishlist = &Apache::lonnet::get('wishlist',\@wishlistkeys); + 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); @@ -68,7 +111,8 @@ sub getWishlist { } } - #If hash is empty, put 'root' into it, so we got a node to start the tree + # 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'; @@ -92,9 +136,64 @@ sub deleteWishlist { } +=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 ($rootgiven, $title, $path, $note) = @_; + + $root = $rootgiven; + @childrenRt = $root->children(); + my $date = gmtime(); # Create Entry-Object my $entry = Entry->new(title => $title, path => $path, note => $note, date => $date); @@ -105,52 +204,69 @@ sub newEntry() { if ($folderIndex ne '') { @allFolders = (); &getFoldersToArray(\@childrenRt); - my $folderToInsertOn = &Tree::getNodeByIndex($folderIndex,\@allFolders); + my $folderToInsertOn = &Apache::Tree::getNodeByIndex($folderIndex,\@allFolders); $folderToInsertOn->add_child($tree); } else { $root->add_child($tree); } - &saveChanges(); + return &saveChanges(); } # Delete entries sub deleteEntries { + my $rootgiven = shift; my $marked = shift; - &getNodesToArray(\@childrenRt); + $root = $rootgiven; + @childrenRt = $root->children(); + + &getNodesToArray(\@childrenRt); foreach my $m (@$marked) { - my $found = &Tree::getNodeByIndex($m, \@allNodes); - &Tree::removeNode($found); + my $found = &Apache::Tree::getNodeByIndex($m, \@allNodes); + # be sure, that entry exists (may have been deleted before, e.g. in an other browsertab) + if (defined $found) { + &Apache::Tree::removeNode($found); + } } @allNodes = (); - &saveChanges(); + return &saveChanges(); } # Sort entries sub sortEntries { + my $rootgiven = shift; my $indexNode = shift; my $at = shift; + + $root = $rootgiven; + @childrenRt = $root->children(); &getNodesToArray(\@childrenRt); - my $foundNode = &Tree::getNodeByIndex($indexNode, \@allNodes); + my $foundNode = &Apache::Tree::getNodeByIndex($indexNode, \@allNodes); - &Tree::moveNode($foundNode,$at,undef); + &Apache::Tree::moveNode($foundNode,$at,undef); @allNodes = (); + return &saveChanges(); } # Move entries sub moveEntries { + my $rootgiven = shift; my $indexNodesToMove = shift; my $indexParent = shift; my @nodesToMove = (); + $root = $rootgiven; + @childrenRt = $root->children(); + + # get all nodes that should be moved &getNodesToArray(\@childrenRt); foreach my $index (@$indexNodesToMove) { - my $foundNode = &Tree::getNodeByIndex($index, \@allNodes); + my $foundNode = &Apache::Tree::getNodeByIndex($index, \@allNodes); push(@nodesToMove, $foundNode); } @@ -166,55 +282,113 @@ sub moveEntries { } if (!$parentIsIn) { if ($indexParent ne "root") { - $foundParent = &Tree::getNodeByIndex($indexParent, \@allNodes); - &Tree::moveNode($node,undef,$foundParent); + $foundParent = &Apache::Tree::getNodeByIndex($indexParent, \@allNodes); + &Apache::Tree::moveNode($node,undef,$foundParent); } else { - &Tree::moveNode($node,undef,$root); + &Apache::Tree::moveNode($node,undef,$root); } } } @allNodes = (); + return &saveChanges(); } # Set a new title for an entry sub setNewTitle { - my ($nodeindex, $newTitle) = @_; + my ($rootgiven, $nodeindex, $newTitle) = @_; + + $root = $rootgiven; + @childrenRt = $root->children(); + &getNodesToArray(\@childrenRt); - my $found = &Tree::getNodeByIndex($nodeindex, \@allNodes); + my $found = &Apache::Tree::getNodeByIndex($nodeindex, \@allNodes); $found->value()->title($newTitle); @allNodes = (); + return &saveChanges(); +} + + +# Set a new path for an entry +sub setNewPath { + my ($rootgiven, $nodeindex, $newPath) = @_; + + $root = $rootgiven; + @childrenRt = $root->children(); + + &getNodesToArray(\@childrenRt); + my $found = &Apache::Tree::getNodeByIndex($nodeindex, \@allNodes); + if ($found->value()->path()) { + $found->value()->path($newPath); + return &saveChanges(); + } + @allNodes = (); + return 0; } # Set a new note for an entry sub setNewNote { - my ($nodeindex, $newNote) = @_; + my ($rootgiven, $nodeindex, $newNote) = @_; + + $root = $rootgiven; + @childrenRt = $root->children(); + &getNodesToArray(\@childrenRt); - my $found = &Tree::getNodeByIndex($nodeindex, \@allNodes); + my $found = &Apache::Tree::getNodeByIndex($nodeindex, \@allNodes); $found->value()->note($newNote); @allNodes = (); + return &saveChanges(); } # Save all changes sub saveChanges { @childrenRt = $root->children(); - &Tree::TreeIndex(\@childrenRt); - &Tree::setCountZero(); - &Tree::RootToHash(\@childrenRt); - &Tree::TreeToHash(\@childrenRt); + &Apache::Tree::TreeIndex(\@childrenRt); + &Apache::Tree::setCountZero(); + &Apache::Tree::RootToHash(\@childrenRt); + &Apache::Tree::TreeToHash(\@childrenRt); &deleteWishlist(); &putWishlist(\%TreeToHash); + return $root; } +=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; -my $foldersOption; sub getFoldersForOption { my $nodes = shift; @@ -235,22 +409,6 @@ sub getFoldersForOption { } -sub getfoldersOption { - if (&getWishlist ne 'error') { - %TreeHash = &getWishlist(); - $root = &Tree::HashToTree(); - @childrenRt = $root->children(); - &getFoldersForOption(\@childrenRt); - my $options = ''.$foldersOption; - $foldersOption = ''; - return $options; - } - else { - return ''; - } -} - - # Put all folder-nodes to an array sub getFoldersToArray { my $children = shift; @@ -279,10 +437,83 @@ sub getNodesToArray { } +=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, numskipped) + + 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). + Side effect: increments the scalar ref: numskipped with a count of items in + Stored Links unavailable for selection, (e.g., now marked obsolete or + inaccessible in Community context). + +=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 * &makePopUpNewLink(title, path) + + Returns the HTML-markup for the pop-up-window 'Add Link'. If this is called up from a browsed resource, the input-fields titel and path are pre-filled with the resources' meta-data-title and it's path. + + +=item * &makePopUpNewFolder() + + Returns the HTML-markup for the pop-up-window 'Add Folder'. + + +=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, + my $startPagePopup = &Apache::loncommon::start_page('Stored Links',undef, {'only_body' => 1, 'js_ready' => 1, 'bgcolor' => '#FFFFFF',}); @@ -292,108 +523,34 @@ sub JSforWishlist { &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/domain/user/...'. + ' 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!'); + 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 $warningMoveS = &mt('You must select at minimum one entry to move!'); + my $warningMoveD = &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;}' - +'else {' - +'window.close();' - +'return true;}}' - +'<\/scr'+'ipt>' - +'$inPageNewLink' - +'$endPagePopup'); - newlinkWin.document.close(); + newlinkWin=window.open('/adm/wishlist?mode=newLink','newlinkWin','width=580,height=350, scrollbars=yes'); } function newFolder() { - newfolderWin=window.open('','newfolderWin','width=580,height=270, scrollbars=yes'); - newfolderWin.document.write('$startPagePopup' - +'