# The LearningOnline Network with CAPA # Utility-routines for wishlist # # $Id: lonwishlist.pm,v 1.18 2013/06/18 13:23:42 bisitz 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. =back =cut package Apache::lonwishlist; use strict; use Apache::lonnet; use Apache::loncommon(); use Apache::lonhtmlcommon; use Apache::lonlocal; use LONCAPA qw(:DEFAULT :match); 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; &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 ($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); # 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 = &Apache::Tree::getNodeByIndex($folderIndex,\@allFolders); $folderToInsertOn->add_child($tree); } else { $root->add_child($tree); } return &saveChanges(); } # Delete entries sub deleteEntries { my $rootgiven = shift; my $marked = shift; $root = $rootgiven; @childrenRt = $root->children(); &getNodesToArray(\@childrenRt); foreach my $m (@$marked) { 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 = (); return &saveChanges(); } # Sort entries sub sortEntries { my $rootgiven = shift; my $indexNode = shift; my $at = shift; $root = $rootgiven; @childrenRt = $root->children(); &getNodesToArray(\@childrenRt); my $foundNode = &Apache::Tree::getNodeByIndex($indexNode, \@allNodes); &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 = &Apache::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 = &Apache::Tree::getNodeByIndex($indexParent, \@allNodes); &Apache::Tree::moveNode($node,undef,$foundParent); } else { &Apache::Tree::moveNode($node,undef,$root); } } } @allNodes = (); return &saveChanges(); } # Set a new title for an entry sub setNewTitle { my ($rootgiven, $nodeindex, $newTitle) = @_; $root = $rootgiven; @childrenRt = $root->children(); &getNodesToArray(\@childrenRt); 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 ($rootgiven, $nodeindex, $newNote) = @_; $root = $rootgiven; @childrenRt = $root->children(); &getNodesToArray(\@childrenRt); my $found = &Apache::Tree::getNodeByIndex($nodeindex, \@allNodes); $found->value()->note($newNote); @allNodes = (); return &saveChanges(); } # Save all changes sub saveChanges { @childrenRt = $root->children(); &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; 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, 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('Stored Links',undef, {'only_body' => 1, 'js_ready' => 1, 'bgcolor' => '#FFFFFF',}); my $endPagePopup = &Apache::loncommon::end_page({'js_ready' => 1}); @allFolders = (); &getFoldersToArray(\@childrenRt); &getFoldersForOption(\@childrenRt); # 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 $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(< indent) { if (displ == '') { row.className = (row.className).replace('LC_hidden',''); } else if (displ != '' && !((row.className).match('LC_hidden'))) { var oldClass = row.className; row.className = oldClass+' LC_hidden'; setDisplayNote(row.id.replace('row','note'),'LC_hidden'); } if (status == 'open' && getImage(row).match('closed')) { row = getNextRowWithIndent(row, getIndent(row)); } else { row = getNextRow(row); } if (row != null) { nextIndent = getIndent(row); } else { nextIndent = indent; } } } setClasses(); var newtitles = document.getElementsByName('newtitle'); if (newtitles.length>0) { var deepestRows = getDeepestRows(); var otherRows = getOtherRows(deepestRows); setDisplaySelect(deepestRows,''); setDisplaySelect(otherRows,'LC_hidden'); } } function selectAction(rowid) { var row = document.getElementById(rowid); var indent = getIndent(row); var checked = getChecked(row); var previousFolderRows = new Array(); if (indent != 0) { previousFolderRows = getPreviousFolderRows(row); } if (getNextRow(row) != null) { var nextIndent = getIndent(getNextRow(row)); row = getNextRow(row); while (nextIndent > indent) { setChecked(row,checked); if (status == 'open' && getImage(row).match('closed')) { row = getNextRowWithIndent(row, getIndent(row)); } else { row = getNextRow(row); } if (row != null) { nextIndent = getIndent(row); } else { nextIndent = indent; } } } if (!checked) { var i = 0; for (i=0;i= indent) { if (nextIndent == indent) { return nextRow; } nextRow = getNextRow(nextRow); if (nextRow == null) { return null; } nextIndent = getIndent(nextRow); } } return nextRow; } function getImage(row) { var childIMG = document.getElementById(row.id.replace('row','img')); if ((childIMG.src).match('closed')) { return 'closed'; } else if ((childIMG.src).match('open')) { return 'open;' } else { return 'link'; } } function setImage(row, status) { var childIMG = document.getElementById(row.id.replace('row','img')); var childIMGFolder = document.getElementById(row.id.replace('row','imgFolder')); childIMG.src = "/adm/lonIcons/arrow."+status+".gif"; childIMGFolder.src="/adm/lonIcons/navmap.folder."+status+".gif"; } function getChecked(row) { var childCHECK = document.getElementById(row.id.replace('row','check')); var checked = childCHECK.checked; return checked; } function setChecked(row,checked) { var childCHECK = document.getElementById(row.id.replace('row','check')); if (!childCHECK.disabled) { childCHECK.checked = checked; } } function getPreviousFolderRows(row) { var previousRow = getPreviousRow(row); var indent = getIndent(previousRow); var kindOfEntry = getImage(previousRow); var rows = new Array(); if (kindOfEntry != 'link') { rows.push(previousRow); } while (indent >0) { previousRow = getPreviousRow(previousRow); if (previousRow != null) { indent = getIndent(previousRow); kindOfEntry = getImage(previousRow); if (kindOfEntry != 'link') { rows.push(previousRow); } } else { indent = 0; } } return rows; } function getDeepestRows() { var row = document.getElementById('row0'); var firstRow = row; var indent = getIndent(row); var maxIndent = indent; while (getNextRow(row) != null) { row = getNextRow(row); indent = getIndent(row); if (indent>maxIndent && !((row.className).match('LC_hidden'))) { maxIndent = indent; } } var deepestRows = new Array(); row = firstRow; var rowIndent; while (getNextRow(row) != null) { rowIndent = getIndent(row); if (rowIndent == maxIndent) { deepestRows.push(row); } row = getNextRow(row); } rowIndent = getIndent(row); if (rowIndent == maxIndent) { deepestRows.push(row); } return deepestRows; } function getOtherRows(deepestRows) { var row = document.getElementById('row0'); var otherRows = new Array(); var isIn = false; while (getNextRow(row) != null) { var i = 0; for (i=0; i < deepestRows.length; i++) { if (row.id == deepestRows[i].id) { isIn = true; } } if (!isIn) { otherRows.push(row); } row = getNextRow(row); isIn = false; } for (i=0; i < deepestRows.length; i++) { if (row.id == deepestRows[i].id) { isIn = true; } } if (!isIn) { otherRows.push(row); } return otherRows; } function setDisplaySelect(deepestRows, displ) { var i = 0; for (i = 0; i < deepestRows.length; i++) { var row = deepestRows[i]; var childSEL = document.getElementById(row.id.replace('row','sel')); childSEL.className = displ; } } function submitSelect() { var list = document.getElementsByName('list')[0]; list.setAttribute("action","/adm/wishlist?mode=edit"); list.submit(); } function setDisplayNote(rowid, displ) { var row = document.getElementById(rowid); if (!displ) { if ((row.className).match('LC_hidden')) { row.className = (row.className).replace('LC_hidden',''); } else { var oldClass = row.className; row.className = oldClass+' LC_hidden'; } } else { if (displ == '') { row.className = (row.className).replace('LC_hidden',''); } else if (displ != '' && !((row.className).match('LC_hidden'))) { var oldClass = row.className; row.className = oldClass+' LC_hidden'; } } var noteText = document.getElementById(rowid.replace('note','noteText')); var noteImg = document.getElementById(rowid.replace('note','noteImg')); if (noteText.value) { noteImg.src = "/res/adm/pages/anot2.png"; } else { noteImg.src = "/res/adm/pages/anot.png"; } } function setClasses() { var row = document.getElementById("row0"); var note = document.getElementById("note0"); var LC_class = 0; if (getNextRow(row) != null) { while (getNextRow(row) != null) { if (!(row.className).match('LC_hidden')) { note.className = (note.className).replace('LC_even_row',''); note.className = (note.className).replace('LC_odd_row',''); if (LC_class) { row.className = 'LC_even_row'; note.className = 'LC_even_row'+note.className; } else { row.className = 'LC_odd_row'; note.className = 'LC_odd_row'+note.className;; } LC_class = !LC_class; } note = getNextNote(row); row = getNextRow(row); } } if (!(row.className).match('LC_hidden')) { note.className = (note.className).replace('LC_even_row',''); note.className = (note.className).replace('LC_odd_row',''); if (LC_class) { row.className = 'LC_even_row'; note.className = 'LC_even_row'+note.className; } else { row.className = 'LC_odd_row'; note.className = 'LC_odd_row'+note.className; } } } function selectDestinationFolder(mode) { var mark = document.getElementsByName('mark'); var i = 0; for (i = 0; i < mark.length; i++) { if (mark[i].checked) { document.getElementsByName('list')[0].submit(); return true; } } if (mode == 'move') { alert('$warningMoveS'); } else { alert('$warningMoveD'); } return false; } function preview(url) { var newWin; if (!(url.match(/^http:\\/\\//) || url.match(/^https:\\/\\//))) { newWin = window.open(url+'?inhibitmenu=yes','preview','width=560,height=350,scrollbars=yes'); } else { newWin = window.open(url,'preview','width=560,height=350,scrollbars=yes'); } newWin.focus(); } function checkAll() { var checkboxes = document.getElementsByName('check'); for (var i = 0; i < checkboxes.length; i++) { if (!checkboxes[i].disabled) { checkboxes[i].checked = "checked"; } } } function uncheckAll() { var checkboxes = document.getElementsByName('check'); for (var i = 0; i < checkboxes.length; i++) { if (!checkboxes[i].disabled) { checkboxes[i].checked = ""; } } } JAVASCRIPT return $js; } sub JSforImport{ my $rat = shift; my $js; if ($rat eq 'simple' || $rat eq '') { $js = &Apache::lonhtmlcommon::scripttag(<value()->nindex(); # start row, use data_table routines to set class to LC_even or LC_odd automatically. this row contains a checkbox, the title and the note-icon. # only display the top level entries on load $wishlistHTMLview .= ($n->parent()->value() eq 'root')?&Apache::loncommon::start_data_table_row('','row'.$index) :&Apache::loncommon::continue_data_table_row('LC_hidden','row'.$index); # checkboxes $wishlistHTMLview .= ''; # entry is a folder if ($n->value()->path() eq '') { $wishlistHTMLview .= ''. ''. ''. 'folder'. $n->value()->title().''; } # entry is a link else { $wishlistHTMLview .= ''. 'value()->path()."'".');">'. 'link'. $n->value()->title().''; } # note-icon, different icons for an entries with note and those without my $noteIMG = 'anot.png'; if ($n->value()->note() ne '') { $noteIMG = 'anot2.png'; } $wishlistHTMLview .= ''. ''.&mt('Note').''; $wishlistHTMLview .= &Apache::loncommon::end_data_table_row(); # start row containing the textarea for the note, do not display note on default $wishlistHTMLview .= &Apache::loncommon::continue_data_table_row('LC_hidden','note'.$index). ''. ''; $wishlistHTMLview .= &Apache::loncommon::end_data_table_row(); # if the entry is a folder, it could have other entries as content. if it has, call wishlistView for those entries my @children = $n->children(); if ($#children >=0) { $indent_view += 20; &wishlistView(\@children); $indent_view -= 20; } } } # HTML-Markup for table if in edit-mode my $wishlistHTMLedit; my $indent_edit = $indentConst; sub wishlistEdit { my $nodes = shift; my $curNode = 1; foreach my $n (@$nodes) { my $index = $n->value()->nindex(); # start row, use data_table routines to set class to LC_even or LC_odd automatically. # this rows contains a checkbox, a select-field for sorting entries, the title in an input-field and the note-icon. # only display the top level entries on load $wishlistHTMLedit .= ($n->parent()->value() eq 'root')?&Apache::loncommon::start_data_table_row('','row'.$index) :&Apache::loncommon::continue_data_table_row('LC_hidden','row'.$index); # checkboxes $wishlistHTMLedit .= ''; # option-tags for sorting entries. we need the numbers from 1 to n with n being the number of entries on the same level as the current entry. # set the number for the current entry into brackets my $options; for (my $i = 1; $i < ((scalar @{$nodes})+1); $i++) { if ($i == $curNode) { $options .= ''; } else { $options .= ''; } } $curNode++; # entry is a folder if ($n->value()->path() eq '') { $wishlistHTMLedit .= ''. ''. ''. ''. 'folder'. ''. ''; } # entry is a link else { $wishlistHTMLedit .= ''. ''. 'link'. ''. ''; } # note-icon, different icons for an entries with note and those without my $noteIMG = 'anot.png'; if ($n->value()->note() ne '') { $noteIMG = 'anot2.png'; } $wishlistHTMLedit .= ''. ''.&mt('Note').''; $wishlistHTMLedit .= &Apache::loncommon::end_data_table_row(); # start row containing the textarea for the note $wishlistHTMLedit .= &Apache::loncommon::continue_data_table_row('LC_hidden','note'.$index). ''. ''; $wishlistHTMLedit .= &Apache::loncommon::end_data_table_row(); # if the entry is a folder, it could have other entries as content. if it has, call wishlistEdit for those entries my @children = $n->children(); if ($#children >=0) { $indent_edit += 20; &wishlistEdit(\@children); $indent_edit -= 20; } } } # HTML-Markup for table if in move-mode my $wishlistHTMLmove =''. ''.&mt('Top level').''; my $indent_move = $indentConst; sub wishlistMove { my $nodes = shift; my $marked = shift; foreach my $n (@$nodes) { my $index = $n->value()->nindex(); #find out wether the current entry was marked to be moved. my $isIn = 0; foreach my $m (@$marked) { if ($index == $m) { $isIn = 1; } } # start row and set class for even or odd row. this rows contains the title and the note-icon and can contain a radio-button $wishlistHTMLmove .= &Apache::loncommon::start_data_table_row('','row'.$index); # entry is a folder if ($n->value()->path() eq '') { # display a radio-button, if the folder was not selected to be moved if (!$isIn) { $wishlistHTMLmove .= ''. ''; } # highlight the title, if the folder was selected to be moved else { $wishlistHTMLmove .= ''. ''; } #arrow- and folder-image, all folders are open, and title $wishlistHTMLmove .= ''. 'folder'. $n->value()->title().''; } # entry is a link else { # higlight the title, if the link was selected to be moved my $highlight = ''; if ($isIn) { $highlight = 'style="color:red;"'; } # link-image and title $wishlistHTMLmove .= ''. ''. 'value()->path()."'".');" '.$highlight.'>'. 'link'. $n->value()->title().''; } # note-icon, different icons for an entries with note and those without my $noteIMG = 'anot.png'; if ($n->value()->note() ne '') { $noteIMG = 'anot2.png'; } $wishlistHTMLmove .= ''. ''.&mt('Note').''; $wishlistHTMLmove .= &Apache::loncommon::end_data_table_row(); # start row containing the textarea for the note, readonly in move-mode $wishlistHTMLmove .= &Apache::loncommon::continue_data_table_row('LC_hidden','note'.$index). ''. ''. &Apache::loncommon::end_data_table_row(); # if the entry is a folder, it could have other entries as content. if it has, call wishlistMove for those entries my @children = $n->children(); if ($#children >=0) { $indent_move += 20; &wishlistMove(\@children, $marked); $indent_move -= 20; } } } # HTML-Markup for table if in import-mode my $wishlistHTMLimport; my $indent_imp = $indentConst; my $form = 1; sub wishlistImport { my ($nodes,$numskipped) = @_; my ($is_community,%nopick); if ($env{'request.course.id'}) { if (&Apache::loncommon::course_type() eq 'Community') { $is_community = 1; } } foreach my $n (@$nodes) { my $index = $n->value()->nindex(); if ($n->value()->path() =~ m{^(/res/$match_domain/$match_username/)}) { if ($is_community) { unless (&Apache::lonnet::allowed('bro',$n->value()->path())) { $nopick{$n->value()->path()} = $n->value()->title(); $$numskipped ++; } } else { unless (&Apache::lonnet::allowed('bre',$n->value()->path())) { $nopick{$n->value()->path()} = $n->value()->title(); $$numskipped ++; } } } # start row, use data_table routines to set class to LC_even or LC_odd automatically. this row contains a checkbox, the title and the note-icon. # only display the top level entries on load $wishlistHTMLimport .= ($n->parent()->value() eq 'root')?&Apache::loncommon::start_data_table_row('','row'.$index) :&Apache::loncommon::continue_data_table_row('LC_hidden','row'.$index); # checkboxes $wishlistHTMLimport .= ''; my ($disabled,$onclick,$image,$style); if ($nopick{$n->value()->path()}) { $disabled = ' disabled="disabled"'; $image = 'wishlist-link-lighter.png'; $style = 'style="color:#808080;"'; } else { $onclick = ' onclick="selectAction('."'row".$index."'".')"'; $image = 'wishlist-link.png'; } $wishlistHTMLimport .= ''. ''. ''. ''; $wishlistHTMLimport .= ''; # entry is a folder if ($n->value()->path() eq '') { $wishlistHTMLimport .= ''. ''. ''. 'folder'. $n->value()->title().''; } # entry is a link else { $wishlistHTMLimport .= ''; unless ($nopick{$n->value()->path()}) { $wishlistHTMLimport .= 'value()->path()."'".');">'; } $wishlistHTMLimport .= 'link'. ''.$n->value()->title().''; $form++; } # note-icon, different icons for an entries with note and those without my $noteIMG = 'anot.png'; if ($n->value()->note() ne '') { $noteIMG = 'anot2.png'; } $wishlistHTMLimport .= ''. ''.&mt('Note').''; $wishlistHTMLimport .= &Apache::loncommon::end_data_table_row(); # start row containing the textarea for the note, do not display note on default, readonly in import-mode $wishlistHTMLimport .= &Apache::loncommon::continue_data_table_row('LC_hidden','note'.$index). ''. ''; $wishlistHTMLimport .= &Apache::loncommon::end_data_table_row(); # if the entry is a folder, it could have other entries as content. if it has, call wishlistImport for those entries my @children = $n->children(); if ($#children >=0) { $indent_imp += 20; &wishlistImport(\@children,$numskipped); $indent_imp -= 20; } } return; } # Returns the HTML-Markup for wishlist sub makePage { my $rootgiven = shift; my $mode = shift; my $marked = shift; $root = $rootgiven; @childrenRt = $root->children(); # breadcrumbs and start_page &Apache::lonhtmlcommon::clear_breadcrumbs(); &Apache::lonhtmlcommon::add_breadcrumb( { href => '/adm/wishlist?mode='.$mode, text => 'Stored Links'}); my $startPage = &Apache::loncommon::start_page('Stored Links',undef, {'add_entries' => { 'onload' => 'javascript:onLoadAction('."'".$mode."'".');', 'onunload' => 'javascript:window.name = '."'loncapaclient'"}}); my $breadcrumbs = &Apache::lonhtmlcommon::breadcrumbs(&mt('Stored Links').&Apache::loncommon::help_open_topic('Wishlist')); # get javascript-code for wishlist-interactions my $js = &JSforWishlist(); # texthash for items in funtionlist my %lt = &Apache::lonlocal::texthash( 'ed' => 'Edit', 'vw' => 'View', 'al' => 'Add Link', 'af' => 'Add Folder', 'mv' => 'Move Selected', 'dl' => 'Delete Selected', 'sv' => 'Save'); # start functionlist my $functions = &Apache::lonhtmlcommon::start_funclist(); # icon for edit-mode, display when in view-mode if ($mode eq 'view') { $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'ed'}.' '. ''.$lt{'ed'}.''); } # icon for view-mode, display when in edit-mode else { $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'vw'}.' '. ''.$lt{'vw'}.''); } # icon for adding a new link $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'al'}.''. ''.$lt{'al'}.''); # icon for adding a new folder $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'af'}.''. ''.$lt{'af'}.''); # icon for moving entries $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'mv'}.''. ''.$lt{'mv'}.''); # icon for deleting entries $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'dl'}.''. ''.$lt{'dl'}.''); # icon for saving changes $functions .= &Apache::lonhtmlcommon::add_item_funclist(''. ''.$lt{'sv'}.''. ''.$lt{'sv'}.''); # end funtionlist and generate subbox $functions.= &Apache::lonhtmlcommon::end_funclist(); my $subbox = &Apache::loncommon::head_subbox($functions); # start form my $inner .= '
'. ''; # only display subbox in view- or edit-mode if ($mode eq 'view' || $mode eq 'edit') { $inner .= $subbox; } # generate table-content depending on mode if ($mode eq 'edit') { &wishlistEdit(\@childrenRt); if ($wishlistHTMLedit ne '') { $inner .= &Apache::loncommon::start_data_table("LC_tableOfContent"); $inner .= $wishlistHTMLedit; $inner .= &Apache::loncommon::end_data_table(); } else { $inner .= ''.&mt("Your Stored Links list is currently empty.").''; } $wishlistHTMLedit = ''; } elsif ($mode eq 'view') { &wishlistView(\@childrenRt); if ($wishlistHTMLview ne '') { $inner .= ''.$wishlistHTMLview.'
'; } else { $inner .= ''.&mt("Your Stored Links list is currently empty.").''; } $wishlistHTMLview = ''; } else { my $markStr = ''; foreach my $m (@$marked) { $markStr .= $m.','; } if ($markStr) { $markStr = substr($markStr, 0, length($markStr)-1); $inner .= ''; $inner .= '

'.&mt('You have selected the red marked entries to be moved to another folder. '. 'Now choose the new destination folder.').'

'; &wishlistMove(\@childrenRt, $marked); $inner .= ''.$wishlistHTMLmove.'


'; $inner .= ''. ''; $wishlistHTMLmove =''. ''.&mt('Top level').''; } else { $inner .= '

'.&mt("You haven't marked any entry to move.").'

'. ''; } } # end form $inner .= '
'; # end_page my $endPage = &Apache::loncommon::end_page(); # put all page-elements together my $page = $startPage.$breadcrumbs.$js.$inner.$endPage; return $page; } # Returns the HTML-Markup for the PopUp, shown when a new link should set, when NOT # beeing in the wishlist-interface (method is called in lonmenu and lonsearchcat) sub makePopUpNewLink { my ($title, $path) = @_; # Get all existing folders to offer posibility to set a new link # into a folder my %TreeHashLink = &Apache::lonwishlist::getWishlist(); my $rootLink = &Apache::Tree::HashToTree(\%TreeHashLink); my @childrenRtLink = $rootLink->children(); $foldersOption = ''; @allFolders = (); &getFoldersToArray(\@childrenRtLink); &getFoldersForOption(\@childrenRtLink); my $options = ''.$foldersOption; $foldersOption = ''; @allFolders = (); # HTML-Markup for the Pop-Up-window 'Set a link for this resource to wishlist' my $startPageWishlistlink = &Apache::loncommon::start_page('Save to Stored Links',undef, {'only_body' => 1, 'bgcolor' => '#FFFFFF',}); my $warningLink = &mt('You must insert a title!'); 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 $inPageWishlistlink1 = '

'.&mt('Save to Stored Links').'

'; # If no title is delivered, 'New Link' is called up from the wishlist-interface, so after # submitting the window should close instead of offering a link to wishlist (like it should do # if we call 'Set New Link' from within a browsed ressource) if (!$title) { $inPageWishlistlink1 .= '
'; } else { $inPageWishlistlink1 .= ''; } $inPageWishlistlink1 .= &Apache::lonhtmlcommon::start_pick_box(). &Apache::lonhtmlcommon::row_title(&mt('Link Title')); my $inPageWishlistlink2 = &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title(&mt('Path')); my $inPageWishlistlink3 = &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title(&mt('Note')). ''. &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::end_pick_box(). '

'. ''. ''. ''. '
'; $options = ''; my $endPageWishlistlink = &Apache::loncommon::end_page(); my $popUp = $startPageWishlistlink. $inPageWishlistlink1. ''. $inPageWishlistlink2. ''. $inPageWishlistlink3; # JavaScript-function to set title and path of ressource automatically # and show warning, if no title was set or path is invalid $popUp .= < document.getElementsByName("title")[0].value = '$title'; document.getElementsByName("path")[0].value = '$path'; var fromwishlist = false; var titleget = '$title'; if (!titleget) { fromwishlist = true; } function newlinksubmit(){ var title = document.getElementsByName("title")[0].value; var path = document.getElementsByName("path")[0].value; if (!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;} if (fromwishlist) { window.close(); } return true;} <\/script> SCRIPT $popUp .= $endPageWishlistlink; return $popUp; } sub makePopUpNewFolder { # Get all existing folders to offer posibility to create a new folder # into an existing folder my %TreeHashLink = &Apache::lonwishlist::getWishlist(); my $rootLink = &Apache::Tree::HashToTree(\%TreeHashLink); my @childrenRtLink = $rootLink->children(); $foldersOption = ''; @allFolders = (); &getFoldersToArray(\@childrenRtLink); &getFoldersForOption(\@childrenRtLink); my $options = ''.$foldersOption; $foldersOption = ''; @allFolders = (); # HTML-Markup for the Pop-Up-window 'New Folder' my $startPageWishlistfolder = &Apache::loncommon::start_page('New Folder',undef, {'only_body' => 1, 'bgcolor' => '#FFFFFF',}); my $warningFolder = &mt('You must insert a title!'); my $inPageNewFolder = '

'.&mt('New Folder').'

'. '
'. &Apache::lonhtmlcommon::start_pick_box(). &Apache::lonhtmlcommon::row_title(&mt('Folder title')). '
'. &Apache::lonhtmlcommon::row_closure(). &Apache::lonhtmlcommon::row_title(&mt('Note')). '
'. &Apache::lonhtmlcommon::row_closure(1). &Apache::lonhtmlcommon::end_pick_box(). '

'. ''. ''. ''. '
'; my $endPageWishlistfolder = &Apache::loncommon::end_page(); my $popUp = $startPageWishlistfolder. $inPageNewFolder; $popUp .= < function newfoldersubmit(){ var title = document.getElementsByName("title")[0].value; if (!title) { alert("$warningFolder"); return false;} else { window.close(); return true;}} <\/script> SCRIPT $popUp .= $endPageWishlistfolder; return $popUp; } # Returns the HTML-Markup for the page, shown when a link was set sub makePageSet { my $title = 'Stored Links'; # start_page my $output = &Apache::loncommon::start_page($title,undef, {'only_body' => 1}) .'

'.&mt($title).'

'; # confirm success and offer link to wishlist $output .= &Apache::loncommon::confirmwrapper( &Apache::lonhtmlcommon::confirm_success( &mt('Link successfully saved!'))) .&Apache::lonhtmlcommon::actionbox( [''.&mt('Go to Stored Links').'', ''.&mt('Close this window').'' ]); # end_page $output .= &Apache::loncommon::end_page(); return $output; } # Returns the HTML-Markup for the page, shown when links should be imported into a course sub makePageImport { my $rootgiven = shift; my $rat = shift; $root = $rootgiven; @childrenRt = $root->children(); # start_page my $startPage = &Apache::loncommon::start_page('Stored Links',undef, {'only_body' => 1}); # get javascript-code for wishlist-interactions my $js = &JSforWishlist(); $js .= &JSforImport($rat); my $inner = '

'.&mt('Import Resources from Stored Links').'

'; if (!$rat) { $inner .= '
    '. '
  • '.&mt('Use the checkboxes corresponding to a folder to '. 'easily check all links within the folder.').'
  • '. '
  • '.&mt('The folder structure itself cannot be imported.').'
  • '. '
  • '.&mt('All checked links will be imported into the current folder of your course.').'
  • '. '
'; } else { $inner .= '
    '. '
  • '.&mt('Use the checkboxes corresponding to a folder to '. 'easily check all links within this folder.').'
  • '. '
  • '.&mt('The folder structure itself cannot be imported.').'
  • '. '
'; } my %wishlist = &getWishlist(); #FIXME Saved string containing all folders in wishlist.db-file (key 'folders') in first version of lonwishlist #After splitting lonwishlist into two modules, this is not necessary anymore. So, dependent from when the wishlist #was first called (i.e. when wishlist.db was created), there might be an entry 'folders' or not. Number of links in #wishlist.db depends on wether this entry exists or not...JW my $fnum; if (defined $wishlist{'folders'}) { $fnum = (keys %wishlist)-2; } else { $fnum = (keys %wishlist)-1; } $inner .= '
'. ''. ''. ''. ''. '

'; # wishlist-table my $numskipped = 0; &wishlistImport(\@childrenRt,\$numskipped); if ($wishlistHTMLimport ne '') { $inner .= ''.$wishlistHTMLimport.'
'; } else { $inner .= ''.&mt("Your Stored Links list is currently empty.").''; } if ($numskipped > 0) { $inner .= '

'.&mt('Note: where a Stored Link is unavailable for import in the current context it is grayed out.').'

'; } $wishlistHTMLimport = ''; $inner .= '
'; # end_page my $endPage = &Apache::loncommon::end_page(); # put all page-elements together my $page = $startPage.$js.$inner.$endPage; return $page; } # Returns the HTML-Markup for error-page sub makeErrorPage { # breadcrumbs and start_page &Apache::lonhtmlcommon::add_breadcrumb( { href => '/adm/wishlist', text => 'Stored Links'}); my $startPage = &Apache::loncommon::start_page('Stored Links'); my $breadcrumbs = &Apache::lonhtmlcommon::breadcrumbs(&mt('Stored Links').&Apache::loncommon::help_open_topic('Wishlist')); &Apache::lonhtmlcommon::clear_breadcrumbs(); # error-message my $inner .= ''.&mt('An error occurred! Please try again later.').''; # end_page my $endPage = &Apache::loncommon::end_page(); # put all page-elements together my $page = $startPage.$breadcrumbs.$inner.$endPage; return $page; } # ----------------------------------------------------- package Tree # Extend CPAN-Module Tree by function like 'moveNode' or 'deleteNode' package Apache::Tree; =pod =head2 Routines from package Tree =over 4 =item * &getNodeByIndex(index, nodes) Searches for a node, specified by the index, in nodes (reference to array) and returns it. =item * &moveNode(node, at, newParent) Moves a given node to a new parent (if new parents is defined) or change the position from a node within its siblings (means sorting, at must be defined). =item * &removeNode(node) Removes a node given by node from the tree. =item * &TreeIndex(children) Sets an index for every node in the tree, beginning with 0. Recursive call starting with all children of the root of the tree (parameter children is reference to an array containing the nodes of the current level). =item * &setCountZero() Resets index counter. =item * &RootToHash(childrenRt) Converts the root-node to a hash-entry: the key is root and values are just the indices of root's children. =item * &TreeToHash(childrenRt) Converts all other nodes in the tree to hash. Each node is one hash-entry where the keys are the index of a node and the values are all other attributes (containing tile, path, note, date and indices for all direct children). Recursive call starting with all children of the root of the tree (parameter childrenRT is reference to an array containing the nodes of the current level). =item * &HashToTree() Converts the hash to a tree. Builds a tree-object for each entry in the hash. Afterwards call &buildTree(node, childrenIn, TreeNodes, TreeHash) to connect the tree-objects. =item * &buildTree(node, childrenIn, TreeNodes, TreeHash) Joins the nodes to a tree. Recursive call starting with root and all children of root (parameter childrenIn is reference to an array containing the nodes indices of the current level). =back =cut # returns the node with a given index from a list of nodes sub getNodeByIndex { my $index = shift; my $nodes = shift; my $found; foreach my $n (@$nodes) { my $curIndex = $n->value()->nindex(); if ($curIndex == $index) { $found = $n; } } return $found; } # moves a given node to a new parent or change the position from a node # within its siblings (sorting) sub moveNode { my $node = shift; my $at = shift; my $newParent = shift; if (!$newParent) { $newParent = $node->parent(); } $node->parent()->remove_child($node); if (defined $at) { $newParent->add_child({at => $at},$node); } else { $newParent->add_child($node); } # updating root's children @childrenRt = $root->children(); } # removes a given node sub removeNode() { my $node = shift; my @children = $node->children(); if ($#children >= 0) { foreach my $c (@children) { &removeNode($c); } } $node->parent()->remove_child($node); # updating root's children @childrenRt = $root->children(); } # set an index for every node in the tree, beginning with 0 my $count = 0; sub TreeIndex { my $children = shift; foreach my $n (@$children) { my @children = $n->children(); $n->value()->nindex($count);$count++; if ($#children>=0) { &TreeIndex(\@children); } } } # reset index counter sub setCountZero { $count = 0; } # convert the tree to a hash # each node is one hash-entry # keys are the indices, values are all other attributes # (containing tile, path, note, date and indices for all direct children) # except for root: the key is root and values are # just the indices of root's children sub RootToHash { my $childrenRt = shift; my @indexarr = (); foreach my $c (@$childrenRt) { push (@indexarr, $c->value()->nindex()); } $TreeToHash{'root'} = [@indexarr]; } sub TreeToHash { my $childrenRt = shift; foreach my $n (@$childrenRt) { my @arrtmp = (); $arrtmp[0] = $n->value()->title(); $arrtmp[1] = $n->value()->path(); $arrtmp[2] = $n->value()->note(); $arrtmp[3] = $n->value()->date(); my @childrenRt = $n->children(); my $co = 4; foreach my $c (@childrenRt) { my $i = $c->value()->nindex(); $arrtmp[$co] = $i; $co++; } $TreeToHash{$n->value()->nindex} = [ @arrtmp]; if ($#childrenRt>=0) { &TreeToHash(\@childrenRt); } } } # convert the hash to a tree # build a tree-object for each entry in the hash # afterwards call &buildTree to connect the tree-objects sub HashToTree { my $TreeHash = shift; my @TreeNodes = (); my $root; foreach my $key (keys %$TreeHash) { if ($key eq 'root') { $root = Tree->new("root"); } elsif ($key ne 'folders') { my @attributes = @{ $$TreeHash{$key} }; my $tmpNode; $tmpNode = Tree->new(Entry->new(title=>$attributes[0], path=>$attributes[1], note=>$attributes[2], date=>$attributes[3], nindex=>$key)); push(@TreeNodes, $tmpNode); # shift all attributes except for # the indices representing the children of a node shift(@attributes); shift(@attributes); shift(@attributes); shift(@attributes); $$TreeHash{$key} = [ @attributes ]; } } # if there are nodes, build up the tree-structure if (defined $$TreeHash{'root'} && $$TreeHash{'root'} ne '') { my @childrenRtIn = @{ $$TreeHash{'root'} }; &buildTree(\$root, \@childrenRtIn,\@TreeNodes,$TreeHash); } return $root; } # join the nodes to a tree sub buildTree { my ($node, $childrenIn, $TreeNodes, $TreeHash) = @_; bless($node, 'Tree'); foreach my $c (@$childrenIn) { my $tmpNode = &getNodeByIndex($c,$TreeNodes); $$node->add_child($tmpNode); my @childrenIn = @{ $$TreeHash{$tmpNode->value()->nindex()} }; &buildTree(\$tmpNode,\@childrenIn,$TreeNodes,$TreeHash); } } # ----------------------------------------------------- package Entry # package that defines the entrys a wishlist could have # i.e. folders and links package Entry; # constructor sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my $self = { @_ }; #set attributes bless($self, $class); return $self; } # getter and setter sub title { my $self = shift; if ( @_ ) { $self->{title} = shift} return $self->{title}; } sub date { my $self = shift; if ( @_ ) { $self->{date} = shift} return $self->{date}; } sub note { my $self = shift; if ( @_ ) { $self->{note} = shift} return $self->{note}; } sub path { my $self = shift; if ( @_ ) { $self->{path} = shift} return $self->{path}; } sub nindex { my $self = shift; if ( @_ ) { $self->{nindex} = shift} return $self->{nindex}; } 1; __END__