--- loncom/interface/lonnavmaps.pm 2004/03/24 22:22:04 1.256 +++ loncom/interface/lonnavmaps.pm 2004/11/10 22:20:50 1.303 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.256 2004/03/24 22:22:04 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.303 2004/11/10 22:20:50 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -35,7 +35,7 @@ use Apache::loncommon(); use Apache::lonmenu(); use Apache::lonlocal; use POSIX qw (floor strftime); -use Data::Dumper; # for debugging, not always used +use Data::Dumper; # for debugging, not always # symbolic constants sub SYMB { return 1; } @@ -83,6 +83,56 @@ my %colormap = # is not yet done and due in less then 24 hours my $hurryUpColor = "#FF0000"; +sub launch_win { + my ($mode,$script,$toplinkitems)=@_; + my $result; + if ($script ne 'no') { + $result.=''; + } + if ($mode eq 'link') { + &add_linkitem($toplinkitems,'launchnav','launch_navmapwin()', + "Launch navigation window"); + } + return $result; +} + +sub close { + if ($ENV{'environment.remotenavmap'} ne 'on') { return ''; } + return(< +window.status='Accessing Nav Control'; +menu=window.open("/adm/rat/empty.html","loncapanav", + "height=600,width=400,scrollbars=1"); +window.status='Closing Nav Control'; +menu.close(); +window.status='Done.'; + +ENDCLOSE +} + +sub update { + if ($ENV{'environment.remotenavmap'} ne 'on') { return ''; } + if (!$ENV{'request.course.id'}) { return ''; } + if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } + return(< + +ENDUPDATE +} + sub handler { my $r = shift; real_handler($r); @@ -111,6 +161,48 @@ sub real_handler { &Apache::loncommon::no_cache($r); $r->send_http_header; + my %toplinkitems=(); + + if ($ENV{QUERY_STRING} eq 'collapseExternal') { + &Apache::lonnet::put('environment',{'remotenavmap' => 'off'}); + &Apache::lonnet::appenv('environment.remotenavmap' => 'off'); + my $menu=&Apache::lonmenu::reopenmenu(); + my $navstatus=&Apache::lonmenu::get_nav_status(); + if ($menu) { + $menu=(<print(<<"ENDSUBM"); + + + + + + +ENDSUBM + return; + } + if ($ENV{QUERY_STRING} eq 'launchExternal') { + &Apache::lonnet::put('environment',{'remotenavmap' => 'on'}); + &Apache::lonnet::appenv('environment.remotenavmap' => 'on'); + } + # Create the nav map my $navmap = Apache::lonnavmaps::navmap->new(); @@ -123,26 +215,43 @@ sub real_handler { $r->print("\n"); $r->print("".&mt('Navigate Course Contents').""); # ------------------------------------------------------------ Get query string - &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register']); + &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework']); # ----------------------------------------------------- Force menu registration my $addentries=''; + my $more_unload; + my $body_only=''; + if ($ENV{'environment.remotenavmap'} eq 'on') { + $r->print(''); +# FIXME need to be smarter to only catch window close events +# $more_unload="collapse()" + $body_only=1; + } if ($ENV{'form.register'}) { - $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). - '" onUnload="'.&Apache::lonmenu::unloadevents().'"'; - $r->print(&Apache::lonmenu::registerurl(1)); + $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). + '" onUnload="'.&Apache::lonmenu::unloadevents().';'. + $more_unload.'"'; + $r->print(&Apache::lonmenu::registerurl(1)); + } else { + $addentries=' onUnload="'.$more_unload.'"'; } # Header $r->print(''. &Apache::loncommon::bodytag('Navigate Course Contents','', - $addentries,'','',$ENV{'form.register'})); + $addentries,$body_only,'', + $ENV{'form.register'})); $r->print(''); - + $r->rflush(); # Check that it's defined if (!($navmap->courseMapDefined())) { + $r->print(&Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT')); $r->print('Coursemap undefined.' . ''); return OK; @@ -172,9 +281,28 @@ sub real_handler { } } + if ($ENV{QUERY_STRING} eq 'launchExternal') { + $r->print(' +
+
'); + $r->print(' + '); + } + + if ($ENV{'environment.remotenavmap'} ne 'on') { + $r->print(&launch_win('link','yes',\%toplinkitems)); + } + if ($ENV{'environment.remotenavmap'} eq 'on') { + &add_linkitem(\%toplinkitems,'closenav','collapse()', + "Close navigation window"); + } + my $jumpToFirstHomework = 0; # Check to see if the student is jumping to next open, do-able problem - if ($ENV{QUERY_STRING} eq 'jumpToFirstHomework') { + if ($ENV{QUERY_STRING} =~ /^jumpToFirstHomework/) { $jumpToFirstHomework = 1; # Find the next homework problem that they can do. my $iterator = $navmap->getIterator(undef, undef, undef, 1); @@ -207,8 +335,9 @@ sub real_handler { $r->print("All homework assignments have been completed.

"); } } else { - $r->print("" . - &mt("Go To My First Homework Problem")."    "); + &add_linkitem(\%toplinkitems,'firsthomework', + 'location.href="navmaps?jumpToFirstHomework"', + "Show Me My First Homework Problem"); } my $suppressEmptySequences = 0; @@ -217,32 +346,53 @@ sub real_handler { # Display only due homework. my $showOnlyHomework = 0; - if ($ENV{QUERY_STRING} eq 'showOnlyHomework') { + if ($ENV{'form.showOnlyHomework'} eq "1") { $showOnlyHomework = 1; $suppressEmptySequences = 1; $filterFunc = sub { my $res = shift; return $res->completable() || $res->is_map(); }; + &add_linkitem(\%toplinkitems,'everything', + 'location.href="navmaps?sort='.$ENV{'form.sort'}.'"', + "Show Everything"); $r->print("

".&mt("Uncompleted Homework")."

"); $ENV{'form.filter'} = ''; $ENV{'form.condition'} = 1; $resource_no_folder_link = 1; } else { - $r->print("" . - &mt("Show Only Uncompleted Homework")."    "); - } - + &add_linkitem(\%toplinkitems,'uncompleted', + 'location.href="navmaps?sort='.$ENV{'form.sort'}. + '&showOnlyHomework=1"', + "Show Only Uncompleted Homework"); + } + + my %selected=($ENV{'form.sort'} => 'selected=on'); + my $sort_html=("
+ + + + + +
"); # renderer call my $renderArgs = { 'cols' => [0,1,2,3], + 'sort' => $ENV{'form.sort'}, 'url' => '/adm/navmaps', 'navmap' => $navmap, 'suppressNavmap' => 1, 'suppressEmptySequences' => $suppressEmptySequences, 'filterFunc' => $filterFunc, 'resource_no_folder_link' => $resource_no_folder_link, - 'r' => $r}; + 'sort_html'=> $sort_html, + 'r' => $r, + 'caller' => 'navmapsdisplay', + 'linkitems' => \%toplinkitems}; my $render = render($renderArgs); - $navmap->untieHashes(); # If no resources were printed, print a reassuring message so the # user knows there was no error. @@ -292,7 +442,7 @@ sub getLinkForResource { foreach $res (@$stack) { if (defined($res)) { if ($res->is_page()) { - return $res->src(); + return $res->link(); } # in case folder was skipped over as "only sequence" my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); @@ -311,7 +461,7 @@ sub getLinkForResource { if (defined($_)) { $res = $_; } } - return $res->src(); + return $res->link(); } # Convenience function: This separates the logic of how to create @@ -512,7 +662,7 @@ sub timeToHumanString { } # Not this year, so show the year - my $timeStr = strftime("on %A, %b %e %G at %I:%M %P", localtime($time)); + my $timeStr = strftime("on %A, %b %e %Y at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return $timeStr; @@ -864,16 +1014,13 @@ sub render_resource { my $filter = $it->{FILTER}; my $title = $resource->compTitle(); - if ($src =~ /^\/uploaded\//) { - $nonLinkedText=$title; - $title = ''; - } + my $partLabel = ""; my $newBranchText = ""; - + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = ""; + $newBranchText = ""; } # links to open and close the folder @@ -881,17 +1028,16 @@ sub render_resource { my $linkclose = ""; # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon = ''; + $icon =''; } else { $icon = $params->{'indentString'}; } } else { - $icon = ""; + $icon = ""; } # Display the correct map icon to open or shut map @@ -906,7 +1052,7 @@ sub render_resource { if (!$params->{'resource_no_folder_link'}) { $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; - $icon = ""; + $icon = ""; $linkopen = "{'queryString'} . '&filter='; @@ -923,7 +1069,7 @@ sub render_resource { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.nomanip.gif'; - $icon = ""; + $icon = ""; $linkopen = ""; $linkclose = ""; @@ -961,9 +1107,7 @@ sub render_resource { if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { - my $displaypart=&Apache::lonnet::EXT('resource.'.$part.'.display', - $resource->symb()); - unless ($displaypart) { $displaypart=$part; } + my $displaypart=$resource->part_display($part); $partLabel = " (Part: $displaypart)"; $link.='#'.&Apache::lonnet::escape($part); $title = ""; @@ -973,9 +1117,12 @@ sub render_resource { $nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; } - if (!$params->{'resource_nolink'} && $src !~ /^\/uploaded\// && - !$resource->is_sequence()) { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + my $target; + if ($ENV{'environment.remotenavmap'} eq 'on') { + $target=' target="loncapaclient" '; + } + if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { + $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } else { $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } @@ -990,10 +1137,10 @@ sub render_communication_status { my $link = $params->{"resourceLink"}; my $linkopen = ""; my $linkclose = ""; - + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($resource->hasDiscussion()) { $discussionHTML = $linkopen . - '' . + '' . $linkclose; } @@ -1003,7 +1150,7 @@ sub render_communication_status { if ($_) { $feedbackHTML .= ' ' - . ''; } } @@ -1018,7 +1165,7 @@ sub render_communication_status { $errorcount++; $errorHTML .= ' ' - . ''; } } @@ -1047,7 +1194,9 @@ sub render_quick_status { my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { - $result .= "$linkopen$alt$linkclose\n"; + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); + $result .= "$linkopen$alt$linkclose\n"; } else { $result .= " \n"; } @@ -1178,11 +1327,17 @@ sub setDefault { return $val; } +sub cmp_title { + my ($atitle,$btitle) = (lc($_[0]->compTitle),lc($_[1]->compTitle)); + $atitle=~s/^\s*//; + $btitle=~s/^\s*//; + return $atitle cmp $btitle; +} + sub render { my $args = shift; &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); my $result = ''; - # Configure the renderer. my $cols = $args->{'cols'}; if (!defined($cols)) { @@ -1263,7 +1418,7 @@ sub render { # We only need to do this if we need to open the maps to show the # current position. This will change the counter so we can't count # for the jump marker with this loop. - while (($curRes = $mapIterator->next()) && !$found) { + while ($here && ($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1339,23 +1494,24 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - + # Print key? if ($printKey) { $result .= ''; my $date=localtime; $result.=''; + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($navmap->{LAST_CHECK}) { $result .= - ' '.&mt('New discussion since').' '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). ''; } else { $result .= ''; } @@ -1363,18 +1519,87 @@ sub render { } if ($printCloseAll && !$args->{'resource_no_folder_link'}) { + my ($link,$text); if ($condition) { - $result.="".&mt('Close All Folders').""; + $link='"navmaps?condition=0&filter=&'.$queryString. + '&here='.&Apache::lonnet::escape($here).'"'; + $text='Close All Folders'; + } else { + $link='"navmaps?condition=1&filter=&'.$queryString. + '&here='.&Apache::lonnet::escape($here).'"'; + $text='Open All Folders'; + } + if ($args->{'caller'} eq 'navmapsdisplay') { + &add_linkitem($args->{'linkitems'},'changefolder', + 'location.href='.$link,$text); + } else { + $result.=''.&mt($text).''; + } + $result .= "\n"; + } + + # Check for any unread discussions in all resources. + if ($args->{'caller'} eq 'navmapsdisplay') { + &add_linkitem($args->{'linkitems'},'clearbubbles', + 'document.clearbubbles.submit()', + 'Mark all posts read'); + my $time=time; + $result .= (< + + +END + if ($args->{'sort'} eq 'discussion') { + my $totdisc = 0; + my $haveDisc = ''; + my @allres=$navmap->retrieveResources(); + foreach my $resource (@allres) { + if ($resource->hasDiscussion()) { + my $ressymb; + if ($resource->symb() =~ m-(___adm/\w+/\w+)/(\d+)/bulletinboard$-) { + $ressymb = 'bulletin___'.$2.$1.'/'.$2.'/bulletinboard'; + } else { + $ressymb = $resource->symb(); + } + $haveDisc .= $ressymb.':'; + $totdisc ++; + } + } + if ($totdisc > 0) { + $haveDisc =~ s/:$//; + $result .= (< + +END + } + } + $result.=''; + } + + if ($args->{'caller'} eq 'navmapsdisplay') { + $result .= '
Key:    '. - ' '.&mt('New message (click to open)').'

'. + ' '.&mt('New message (click to open)').'

'. '

  '. - ' '.&mt('Discussions').''. - '   '.&mt('New message (click to open)'). + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). '
'; + if ($ENV{'environment.remotenavmap'} ne 'on') { + $result .= ''; } else { - $result.="".&mt('Open All Folders').""; + $result .= ''; } - $result .= "

\n"; - } + $result.=&show_linkitems($args->{'linkitems'}); + if ($args->{'sort_html'}) { + if ($ENV{'environment.remotenavmap'} ne 'on') { + $result.=''. + ''; + } else { + $result.=''; + } + } + $result .= '
'. + &Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT').' 
   '.$args->{'sort_html'}.'

'. + $args->{'sort_html'}.'
'; + } elsif ($args->{'sort_html'}) { + $result.=$args->{'sort_html'}; + } + $result .= "
\n"; if ($r) { $r->print($result); $r->rflush(); @@ -1396,7 +1621,9 @@ sub render { $args->{'indentLevel'} = 0; $args->{'isNewBranch'} = 0; $args->{'condensed'} = 0; - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); + $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1446,7 +1673,60 @@ sub render { $args->{'here'} = $here; $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0 - while ($curRes = $it->next($closeAllPages)) { + my @resources; + my $code='';# sub { !(shift->is_map();) }; + if ($args->{'sort'} eq 'title') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if ($res->is_map()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } elsif ($args->{'sort'} eq 'duedate') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->is_problem()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { + if ($a->duedate ne $b->duedate) { + return $a->duedate cmp $b->duedate; + } + my $value=&cmp_title($a,$b); + return $value; + } @resources; + } elsif ($args->{'sort'} eq 'discussion') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->hasDiscussion() && + !$res->getFeedback() && + !$res->getErrors()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } else { + #unknow sort mechanism or default + undef($args->{'sort'}); + } + + + while (1) { + if ($args->{'sort'}) { + $curRes = shift(@resources); + } else { + $curRes = $it->next($closeAllPages); + } + if (!$curRes) { last; } + # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || $curRes == $it->BEGIN_BRANCH() ) { @@ -1560,18 +1840,23 @@ sub render { # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; - my $stack = $it->getStack(); - my $src = getLinkForResource($stack); + my $src; + if ($args->{'sort'}) { + $src = $curRes->src(); # FIXME this is wrong for .pages + } else { + my $stack = $it->getStack(); + $src=getLinkForResource($stack); + } my $anchor=''; - if ($src=~s/(\#.*$)//) { + if ($src=~s/(\#.*)$//) { $anchor=$1; } my $srcHasQuestion = $src =~ /\?/; $args->{"resourceLink"} = $src. ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($curRes->symb()). + 'symb=' . &Apache::lonnet::escape($curRes->shown_symb()). $anchor; - + # Now, display each column. foreach my $col (@$cols) { my $colHTML = ''; @@ -1637,8 +1922,46 @@ if (location.href.indexOf('#curloc')==-1 $r->rflush(); } - if ($mustCloseNavMap) { $navmap->untieHashes(); } + return $result; +} + +sub add_linkitem { + my ($linkitems,$name,$cmd,$text)=@_; + $$linkitems{$name}{'cmd'}=$cmd; + $$linkitems{$name}{'text'}=&mt($text); +} +sub show_linkitems { + my ($linkitems)=@_; + my @linkorder = ("launchnav","closenav","firsthomework","everything", + "uncompleted","changefolder","clearbubbles"); + + my $result .= (< + +
+   +
'."\n"; + return $result; } @@ -1699,10 +2022,6 @@ successful, or B if not. =back -When you are done with the $navmap object, you I call -$navmap->untieHashes(), or you'll prevent the current user from using that -course until the web server is restarted. (!) - =head2 Methods =over 4 @@ -1817,6 +2136,7 @@ sub generate_course_user_opt { sub generate_email_discuss_status { my $self = shift; + my $symb = shift; if ($self->{EMAIL_DISCUSS_GENERATED}) { return; } my $cid=$ENV{'request.course.id'}; @@ -1829,6 +2149,15 @@ sub generate_email_discuss_status { $courseLeaveTime : $logoutTime); my %discussiontime = &Apache::lonnet::dump('discussiontimes', $cdom, $cnum); + my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss', + $ENV{'user.domain'},$ENV{'user.name'},'lastread'); + my %lastreadtime = (); + foreach (keys %lastread) { + my $key = $_; + $key =~ s/_lastread$//; + $lastreadtime{$key} = $lastread{$_}; + } + my %feedback=(); my %error=(); my $keys = &Apache::lonnet::reply('keys:'. @@ -1838,17 +2167,11 @@ sub generate_email_discuss_status { foreach my $msgid (split(/\&/, $keys)) { $msgid=&Apache::lonnet::unescape($msgid); - my $plain=&Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); - if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { - my ($what,$url)=($1,$2); - my %status= - &Apache::lonnet::get('email_status',[$msgid]); - if ($status{$msgid}=~/^error\:/) { - $status{$msgid}=''; - } - - if (($status{$msgid} eq 'new') || - (!$status{$msgid})) { + if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { + my $plain= + &Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); + if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { + my ($what,$url)=($1,$2); if ($what eq 'Error') { $error{$url}.=','.$msgid; } else { @@ -1858,10 +2181,13 @@ sub generate_email_discuss_status { } } + #url's of resources that have feedbacks $self->{FEEDBACK} = \%feedback; - $self->{ERROR_MSG} = \%error; # what is this? JB + #or errors + $self->{ERROR_MSG} = \%error; $self->{DISCUSSION_TIME} = \%discussiontime; $self->{EMAIL_STATUS} = \%emailstatus; + $self->{LAST_READ} = \%lastreadtime; $self->{EMAIL_DISCUSS_GENERATED} = 1; } @@ -1912,13 +2238,6 @@ sub getIterator { return $iterator; } -# unties the hash when done -sub untieHashes { - my $self = shift; - untie %{$self->{NAV_HASH}}; - untie %{$self->{PARM_HASH}}; -} - # Private method: Does the given resource (as a symb string) have # current discussion? Returns 0 if chat/mail data not extracted. sub hasDiscussion { @@ -1930,8 +2249,21 @@ sub hasDiscussion { if (!defined($self->{DISCUSSION_TIME})) { return 0; } #return defined($self->{DISCUSSION_TIME}->{$symb}); - return $self->{DISCUSSION_TIME}->{$symb} > - $self->{LAST_CHECK}; + +# backward compatibility (bulletin boards used to be 'wrapped') + my $ressymb = $symb; + if ($ressymb =~ m|adm/(\w+)/(\w+)/(\d+)/bulletinboard$|) { + unless ($ressymb =~ m|adm/wrapper/adm|) { + $ressymb = 'bulletin___'.$3.'___adm/wrapper/adm/'.$1.'/'.$2.'/'.$3.'/bulletinboard'; + } + } + + if ( defined ( $self->{LAST_READ}->{$ressymb} ) ) { + return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_READ}->{$ressymb}; + } else { +# return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_CHECK}; # v.1.1 behavior + return $self->{DISCUSSION_TIME}->{$ressymb} > 0; # in 1.2 will display speech bubble icons for all items with posts until marked as read (even if read in v 1.1). + } } # Private method: Does the given resource (as a symb string) have @@ -2002,9 +2334,14 @@ sub getById { sub getBySymb { my $self = shift; my $symb = shift; + my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb); my $map = $self->getResourceByUrl($mapUrl); - return $self->getById($map->map_pc() . '.' . $id); + my $returnvalue = undef; + if (ref($map)) { + $returnvalue = $self->getById($map->map_pc() .'.'.$id); + } + return $returnvalue; } sub getByMapPc { @@ -2198,6 +2535,7 @@ in the filter function. =cut + sub getResourceByUrl { my $self = shift; my $resUrl = shift; @@ -2280,7 +2618,7 @@ sub hasResource { 1; package Apache::lonnavmaps::iterator; - +use WeakRef; =pod =back @@ -2420,7 +2758,7 @@ sub new { my $class = ref($proto) || $proto; my $self = {}; - $self->{NAV_MAP} = shift; + weaken($self->{NAV_MAP} = shift); return undef unless ($self->{NAV_MAP}); # Handle the parameters @@ -2756,7 +3094,7 @@ sub populateStack { 1; package Apache::lonnavmaps::DFSiterator; - +use WeakRef; # Not documented in the perldoc: This is a simple iterator that just walks # through the nav map and presents the resources in a depth-first search # fashion, ignorant of conditionals, randomized resources, etc. It presents @@ -2784,7 +3122,7 @@ sub new { my $class = ref($proto) || $proto; my $self = {}; - $self->{NAV_MAP} = shift; + weaken($self->{NAV_MAP} = shift); return undef unless ($self->{NAV_MAP}); $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); @@ -2938,7 +3276,7 @@ sub populateStack { 1; package Apache::lonnavmaps::resource; - +use WeakRef; use Apache::lonnet; =pod @@ -3020,7 +3358,7 @@ sub new { my $class = ref($proto) || $proto; my $self = {}; - $self->{NAV_MAP} = shift; + weaken($self->{NAV_MAP} = shift); $self->{ID} = shift; # Store this new resource in the parent nav map's cache. @@ -3108,6 +3446,7 @@ Returns the title of the resource. # These info functions can be used directly, as they don't return # resource information. sub comesfrom { my $self=shift; return $self->navHash("comesfrom_", 1); } +sub encrypted { my $self=shift; return $self->navHash("encrypted_", 1); } sub ext { my $self=shift; return $self->navHash("ext_", 1) eq 'true:'; } sub from { my $self=shift; return $self->navHash("from_", 1); } # considered private and undocumented @@ -3119,10 +3458,20 @@ sub randompick { return $self->{NAV_MAP}->{PARM_HASH}->{$self->symb . '.0.parameter_randompick'}; } +sub link { + my $self=shift; + if ($self->encrypted()) { return &Apache::lonenc::encrypted($self->src); } + return $self->src; +} sub src { my $self=shift; return $self->navHash("src_", 1); } +sub shown_symb { + my $self=shift; + if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->symb());} + return $self->symb(); +} sub symb { my $self=shift; (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; @@ -3141,6 +3490,16 @@ sub title { return $self->navHash("title_", 1); } # considered private and undocumented sub to { my $self=shift; return $self->navHash("to_", 1); } +sub condition { + my $self=shift; + my $undercond=$self->navHash("undercond_", 1); + if (!defined($undercond)) { return 1; }; + my $condid=$self->navHash("condid_$undercond"); + if (!defined($condid)) { return 1; }; + my $condition=&Apache::lonnet::directcondval($condid); + return $condition; +} + sub compTitle { my $self = shift; my $title = $self->title(); @@ -3222,6 +3581,23 @@ sub is_sequence { return $self->navHash("is_map_", 1) && $self->navHash("map_type_" . $self->map_pc()) eq 'sequence'; } +sub is_survey { + my $self = shift(); + my $part = shift(); + if ($self->parmval('type',$part) eq 'survey') { + return 1; + } + if ($self->src() =~ /\.(survey)$/) { + return 1; + } + return 0; +} + +sub is_empty_sequence { + my $self=shift; + my $src = $self->src(); + return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc()); +} # Private method: Shells out to the parmval in the nav map, handler parts. sub parmval { @@ -3398,6 +3774,11 @@ sub awarded { } sub duedate { (my $self, my $part) = @_; + my $interval=$self->parmval("interval", $part); + if ($interval) { + my $first_access=&Apache::lonnet::get_first_access('map',$self->symb); + if ($first_access) { return ($first_access+$interval); } + } return $self->parmval("duedate", $part); } sub maxtries { @@ -3441,7 +3822,16 @@ sub weight { $self->symb(), $ENV{'user.domain'}, $ENV{'user.name'}, $ENV{'request.course.sec'}); - +} +sub part_display { + my $self= shift(); my $partID = shift(); + if (! defined($partID)) { $partID = '0'; } + my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display', + $self->symb); + if (! defined($display) || $display eq '') { + $display = $partID; + } + return $display; } # Multiple things need this @@ -3531,6 +3921,16 @@ Returns the number of parts of the probl for single part problems, returns 1. For multipart, it returns the number of parts in the problem, not including psuedo-part 0. +=item * B(): + +Returns the total number of responses in the problem a student can answer. + +=item * B(): + +Returns a hash whose keys are the response types. The values are the number +of times each response type is used. This is for the I problem, not +just a single part. + =item * B(): Returns true if the problem is multipart, false otherwise. Use this instead @@ -3577,6 +3977,26 @@ sub countParts { return scalar(@{$parts}); # + $delta; } +sub countResponses { + my $self = shift; + my $count; + foreach my $part (@{$self->parts()}) { + $count+= scalar($self->responseIds($part)); + } + return $count; +} + +sub responseTypes { + my $self = shift; + my %responses; + foreach my $part ($self->parts()) { + foreach my $responsetype ($self->responseType($part)) { + $responses{$responsetype}++ if (defined($responsetype)); + } + } + return %responses; +} + sub multipart { my $self = shift; return $self->countParts() > 1; @@ -3664,6 +4084,7 @@ sub extractParts { } + # These hashes probably do not need names that end with "Hash".... my %responseIdHash; my %responseTypeHash; @@ -3698,6 +4119,30 @@ sub extractParts { } } } + my $resorder = &Apache::lonnet::metadata($self->src(),'responseorder'); + # + # Reorder the arrays in the %responseIdHash and %responseTypeHash + if ($resorder) { + my @resorder=split(/,/,$resorder); + foreach my $part (keys(%responseIdHash)) { + my $i=0; + my %resids = map { ($_,$i++) } @{ $responseIdHash{$part} }; + my @neworder; + foreach my $possibleid (@resorder) { + if (exists($resids{$possibleid})) { + push(@neworder,$resids{$possibleid}); + } + } + my @ids; + my @type; + foreach my $element (@neworder) { + push (@ids,$responseIdHash{$part}->[$element]); + push (@type,$responseTypeHash{$part}->[$element]); + } + $responseIdHash{$part}=\@ids; + $responseTypeHash{$part}=\@type; + } + } $self->{RESPONSE_IDS} = \%responseIdHash; $self->{RESPONSE_TYPES} = \%responseTypeHash; } @@ -3891,6 +4336,7 @@ sub getCompletionStatus { # Left as separate if statements in case we ever do more with this if ($status eq 'correct_by_student') {return $self->CORRECT;} + if ($status eq 'correct_by_scantron') {return $self->CORRECT;} if ($status eq 'correct_by_override') {return $self->CORRECT_BY_OVERRIDE; } if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } @@ -4041,7 +4487,7 @@ sub status { if ($dateStatus == PAST_DUE_ANSWER_LATER || $dateStatus == PAST_DUE_NO_ANSWER ) { - return $dateStatus; + return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus; } if ($dateStatus == ANSWER_OPEN) { @@ -4227,6 +4673,7 @@ sub getNext { my $to = $self->to(); foreach my $branch ( split(/,/, $to) ) { my $choice = $self->{NAV_MAP}->getById($branch); + if (!$choice->condition()) { next; } my $next = $choice->goesto(); $next = $self->{NAV_MAP}->getById($next);