--- loncom/interface/lonnavmaps.pm 2005/02/17 08:29:43 1.316 +++ loncom/interface/lonnavmaps.pm 2006/12/24 22:13:19 1.395 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.316 2005/02/17 08:29:43 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.395 2006/12/24 22:13:19 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,15 +30,16 @@ package Apache::lonnavmaps; use strict; -use Apache::Constants qw(:common :http); +use GDBM_File; use Apache::loncommon(); -use Apache::lonmenu(); use Apache::lonenc(); use Apache::lonlocal; use Apache::lonnet; use POSIX qw (floor strftime); use Data::Dumper; # for debugging, not always use Time::HiRes qw( gettimeofday tv_interval ); +use lib '/home/httpd/lib/perl/'; +use LONCAPA; # symbolic constants sub SYMB { return 1; } @@ -56,6 +57,7 @@ my %statusIconMap = $resObj->CLOSED => '', $resObj->OPEN => 'navmap.open.gif', $resObj->CORRECT => 'navmap.correct.gif', + $resObj->PARTIALLY_CORRECT => 'navmap.partial.gif', $resObj->INCORRECT => 'navmap.wrong.gif', $resObj->ATTEMPTED => 'navmap.ellipsis.gif', $resObj->ERROR => '' @@ -80,36 +82,15 @@ my %colormap = $resObj->OPEN => '', $resObj->NOTHING_SET => '', $resObj->ATTEMPTED => '', - $resObj->ANSWER_SUBMITTED => '' + $resObj->ANSWER_SUBMITTED => '', + $resObj->PARTIALLY_CORRECT => '#006600' ); # And a special case in the nav map; what to do when the assignment # 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 ''; } + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } return(< window.status='Accessing Nav Control'; @@ -123,8 +104,8 @@ ENDCLOSE } sub update { - if ($ENV{'environment.remotenavmap'} ne 'on') { return ''; } - if (!$ENV{'request.course.id'}) { return ''; } + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + if (!$env{'request.course.id'}) { return ''; } if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } return(< @@ -136,286 +117,6 @@ this.document.navform.submit(); ENDUPDATE } -sub handler { - my $r = shift; - real_handler($r); -} - -sub real_handler { - my $r = shift; - #my $t0=[&gettimeofday()]; - # Handle header-only request - if ($r->header_only) { - if ($ENV{'browser.mathml'}) { - &Apache::loncommon::content_type($r,'text/xml'); - } else { - &Apache::loncommon::content_type($r,'text/html'); - } - $r->send_http_header; - return OK; - } - - # Send header, don't cache this page - if ($ENV{'browser.mathml'}) { - &Apache::loncommon::content_type($r,'text/xml'); - } else { - &Apache::loncommon::content_type($r,'text/html'); - } - &Apache::loncommon::no_cache($r); - $r->send_http_header; - - my %toplinkitems=(); - &add_linkitem(\%toplinkitems,'blank','',"Select Action"); - 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"); - $html - - - - - -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(); - - if (!defined($navmap)) { - my $requrl = $r->uri; - $ENV{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; - } - my $html=&Apache::lonxml::xmlbegin(); - $r->print("$html\n"); - $r->print("".&mt('Navigate Course Contents').""); -# ------------------------------------------------------------ Get query string - &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework','postsymb']); - -# ----------------------------------------------------- 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().';'. - $more_unload.'"'; - $r->print(&Apache::lonmenu::registerurl(1)); - } else { - $addentries=' onUnload="'.$more_unload.'"'; - } - - # Header - $r->print(''. - &Apache::loncommon::bodytag('Navigate Course Contents','', - $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; - } - - # See if there's only one map in the top-level, if we don't - # already have a filter... if so, automatically display it - # (older code; should use retrieveResources) - if ($ENV{QUERY_STRING} !~ /filter/) { - my $iterator = $navmap->getIterator(undef, undef, undef, 0); - my $curRes; - my $sequenceCount = 0; - my $sequenceId; - while ($curRes = $iterator->next()) { - if (ref($curRes) && $curRes->is_sequence()) { - $sequenceCount++; - $sequenceId = $curRes->map_pc(); - } - } - - if ($sequenceCount == 1) { - # The automatic iterator creation in the render call - # will pick this up. We know the condition because - # the defined($ENV{'form.filter'}) also ensures this - # is a fresh call. - $ENV{'form.filter'} = "$sequenceId"; - } - } - - 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} =~ /^jumpToFirstHomework/) { - $jumpToFirstHomework = 1; - # Find the next homework problem that they can do. - my $iterator = $navmap->getIterator(undef, undef, undef, 1); - my $curRes; - my $foundDoableProblem = 0; - my $problemRes; - - while (($curRes = $iterator->next()) && !$foundDoableProblem) { - if (ref($curRes) && $curRes->is_problem()) { - my $status = $curRes->status(); - if ($curRes->completable()) { - $problemRes = $curRes; - $foundDoableProblem = 1; - - # Pop open all previous maps - my $stack = $iterator->getStack(); - pop @$stack; # last resource in the stack is the problem - # itself, which we don't need in the map stack - my @mapPcs = map {$_->map_pc()} @$stack; - $ENV{'form.filter'} = join(',', @mapPcs); - - # Mark as both "here" and "jump" - $ENV{'form.postsymb'} = $curRes->symb(); - } - } - } - - # If we found no problems, print a note to that effect. - if (!$foundDoableProblem) { - $r->print("All homework assignments have been completed.

"); - } - } else { - &add_linkitem(\%toplinkitems,'firsthomework', - 'location.href="navmaps?jumpToFirstHomework"', - "Show Me My First Homework Problem"); - } - - my $suppressEmptySequences = 0; - my $filterFunc = undef; - my $resource_no_folder_link = 0; - - # Display only due homework. - my $showOnlyHomework = 0; - 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 { - &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, - 'sort_html'=> $sort_html, - 'r' => $r, - 'caller' => 'navmapsdisplay', - 'linkitems' => \%toplinkitems}; - my $render = render($renderArgs); - - # If no resources were printed, print a reassuring message so the - # user knows there was no error. - if ($renderArgs->{'counter'} == 0) { - if ($showOnlyHomework) { - $r->print("

".&mt("All homework is currently completed").".

"); - } else { # both jumpToFirstHomework and normal use the same: course must be empty - $r->print("

This course is empty.

"); - } - } - #my $td=&tv_interval($t0); - #$r->print("
$td"); - - $r->print(""); - $r->rflush(); - - return OK; -} - # Convenience functions: Returns a string that adds or subtracts # the second argument from the first hash, appropriate for the # query string that determines which folders to recurse on @@ -448,15 +149,15 @@ sub getLinkForResource { if (defined($res)) { my $anchor; if ($res->is_page()) { - foreach (@$stack) { if (defined($_)) { $anchor = $_; } } - $anchor=&Apache::lonnet::escape($anchor->shown_symb()); + foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } + $anchor=&escape($anchor->shown_symb()); return ($res->link(),$res->shown_symb(),$anchor); } # in case folder was skipped over as "only sequence" my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); if ($map=~/\.page$/) { my $url=&Apache::lonnet::clutter($map); - $anchor=&Apache::lonnet::escape($src->shown_symb()); + $anchor=&escape($src->shown_symb()); return ($url,$res->shown_symb(),$anchor); } } @@ -466,8 +167,8 @@ sub getLinkForResource { # (when we first recurse on a map, it puts an undefined resource # on the bottom because $self->{HERE} isn't defined yet, and we # want the src for the map anyhow) - foreach (@$stack) { - if (defined($_)) { $res = $_; } + foreach my $item (@$stack) { + if (defined($item)) { $res = $item; } } return ($res->link(),$res->shown_symb()); @@ -491,22 +192,23 @@ sub getDescription { return &mt("Not currently assigned."); } if ($status == $res->OPEN_LATER) { - return "Open " . timeToHumanString($res->opendate($part)); + return "Open " . timeToHumanString($res->opendate($part),'start'); } if ($status == $res->OPEN) { if ($res->duedate($part)) { - return &mt("Due")." " .timeToHumanString($res->duedate($part)); + return &mt("Due")." " .timeToHumanString($res->duedate($part),'end'); } else { return &mt("Open, no due date"); } } if ($status == $res->PAST_DUE_ANSWER_LATER) { - return &mt("Answer open")." " . timeToHumanString($res->answerdate($part)); + return &mt("Answer open")." " . timeToHumanString($res->answerdate($part),'start'); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return &mt("Was due")." " . timeToHumanString($res->duedate($part)); + return &mt("Was due")." " . timeToHumanString($res->duedate($part),'end'); } - if ($status == $res->ANSWER_OPEN) { + if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) + && $res->handgrade($part) ne 'yes') { return &mt("Answer available"); } if ($status == $res->EXCUSED) { @@ -526,7 +228,7 @@ sub getDescription { } } if ($res->duedate($part)) { - return &mt("Due")." " . timeToHumanString($res->duedate($part)) . + return &mt("Due")." " . timeToHumanString($res->duedate($part),'end') . " $triesString"; } else { return &mt("No due date")." $triesString"; @@ -564,10 +266,10 @@ sub lastTry { $res->duedate($part) > time(); } -# This puts a human-readable name on the ENV variable. +# This puts a human-readable name on the env variable. sub advancedUser { - return $ENV{'request.role.adv'}; + return $env{'request.role.adv'}; } @@ -579,8 +281,11 @@ sub advancedUser { # print "Answer available $timestring" # Very, very, very, VERY English-only... goodness help a localizer on # this func... + + sub timeToHumanString { - my ($time) = @_; + my ($time,$type,$format) = @_; + # zero, '0' and blank are bad times if (!$time) { return &mt('never'); @@ -651,30 +356,44 @@ sub timeToHumanString { return "$prefix$hourString$minuteString$tense"; } + # If there's a caller supplied format, use it. + + if($format ne '') { + my $timeStr = strftime($format, localtime($time)); + return $timeStr.&Apache::lonlocal::gettimezone($time); + } + # Less then 5 days away, display day of the week and # HH:MM + if ( $delta < $day * 5 ) { my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "next ") . - $timeStr; + return ($inPast ? "last " : "this ") . + $timeStr.&Apache::lonlocal::gettimezone($time); } + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } # Is it this year? if ( $time[5] == $now[5]) { # Return on Month Day, HH:MM meridian - my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); + my $timeStr = strftime("$conjunction %A, %b %e at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return $timeStr; + return $timeStr.&Apache::lonlocal::gettimezone($time); } # Not this year, so show the year - my $timeStr = strftime("on %A, %b %e %Y at %I:%M %P", localtime($time)); + my $timeStr = strftime("$conjunction %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; + return $timeStr.&Apache::lonlocal::gettimezone($time); } } @@ -870,13 +589,13 @@ automatically. =over 4 -=item * B: default: constructs one from %ENV +=item * B: default: constructs one from %env A reference to a fresh ::iterator to use from the navmaps. The rendering will reflect the options passed to the iterator, so you can use that to just render a certain part of the course, if you like. If one is not passed, the renderer will attempt to construct one from -ENV{'form.filter'} and ENV{'form.condition'} information, plus the +env{'form.filter'} and env{'form.condition'} information, plus the 'iterator_map' parameter if any. =item * B: default: not used @@ -886,11 +605,11 @@ instruct the renderer to render only a p the source of the map you want to process, like '/res/103/jerf/navmap.course.sequence'. -=item * B: default: constructs one from %ENV +=item * B: default: constructs one from %env A reference to a navmap, used only if an iterator is not passed in. If this is necessary to make an iterator but it is not passed in, a new -one will be constructed based on ENV info. This is useful to do basic +one will be constructed based on env info. This is useful to do basic error checking before passing it off to render. =item * B: default: must be passed in @@ -916,12 +635,12 @@ then only one line will be displayed for all parts will always be displayed. If showParts is 0, this is ignored. -=item * B: default: determined from %ENV +=item * B: default: determined from %env A string identifying the URL to place the anchor 'curloc' at. It is the responsibility of the renderer user to ensure that the #curloc is in the URL. By default, determined through -the use of the ENV{} 'jump' information, and should normally "just +the use of the env{} 'jump' information, and should normally "just work" correctly. =item * B: default: empty string @@ -949,7 +668,7 @@ are allowing the user to open and close Describes the currently-open row number to cause the browser to jump to, because the user just opened that folder. By default, pulled from -the Jump information in the ENV{'form.*'}. +the Jump information in the env{'form.*'}. =item * B: default: false @@ -1024,7 +743,6 @@ sub render_resource { # it will be quoted with ' in the href. my ($left,$right) = split(/\?/, $link); - $left =~ s/'/\\'/g; $link = $left.'?'.$right; my $src = $resource->src(); @@ -1038,28 +756,34 @@ sub render_resource { my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = ""; + $newBranchText = "Branch"; } # links to open and close the folder - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon =''; + $icon = ''.&mt('Task');
+	    } else {
+		$icon .= 'problem.gif'; } else { $icon = $params->{'indentString'}; } } else { - $icon = ""; + $icon = "  "; } # Display the correct map icon to open or shut map @@ -1071,28 +795,31 @@ sub render_resource { } my $folderType = $resource->is_sequence() ? 'folder' : 'page'; - + my $title=$resource->title; + $title=~s/\"/\"/g; if (!$params->{'resource_no_folder_link'}) { $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; - $icon = ""; + $icon = "\""."; - $linkopen = "{'queryString'} . '&filter='; + $linkopen = "{'url'} . '?' . + $params->{'queryString'} . '&filter='; $linkopen .= ($nowOpen xor $it->{CONDITION}) ? addToFilter($filter, $mapId) : removeFromFilter($filter, $mapId); - $linkopen .= "&condition=" . $it->{CONDITION} . '&hereType=' - . $params->{'hereType'} . '&here=' . - &Apache::lonnet::escape($params->{'here'}) . - '&jump=' . - &Apache::lonnet::escape($resource->symb()) . - "&folderManip=1'>"; + $linkopen .= "&condition=" . $it->{CONDITION} . '&hereType=' + . $params->{'hereType'} . '&here=' . + &escape($params->{'here'}) . + '&jump=' . + &escape($resource->symb()) . + "&folderManip=1\">"; } else { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.nomanip.gif'; - $icon = ""; + $icon = "\""."; $linkopen = ""; $linkclose = ""; @@ -1100,11 +827,14 @@ sub render_resource { } if ($resource->randomout()) { - $nonLinkedText .= ' (hidden) '; + $nonLinkedText .= ' ('.&mt('hidden').') '; + } + if (!$resource->condval()) { + $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; } # We're done preparing and finally ready to start the rendering - my $result = ""; + my $result = ""; my $indentLevel = $params->{'indentLevel'}; if ($newBranchText) { $indentLevel--; } @@ -1124,7 +854,7 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '> '; + $curMarkerBegin = '>'; $curMarkerEnd = '<'; $params->{'displayedHereMarker'} = 1; } @@ -1132,21 +862,21 @@ sub render_resource { if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { my $displaypart=$resource->part_display($part); - $partLabel = " (Part: $displaypart)"; - if ($link!~/\#/) { $link.='#'.&Apache::lonnet::escape($part); } + $partLabel = " (".&mt('Part: [_1]', $displaypart).")"; + if ($link!~/\#/) { $link.='#'.&escape($part); } $title = ""; } if ($params->{'condensed'} && $resource->countParts() > 1) { - $nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; + $nonLinkedText .= ' ('.&mt('[_1] parts', $resource->countParts()).')'; } my $target; - if ($ENV{'environment.remotenavmap'} eq 'on') { + 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"; + $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } else { $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } @@ -1159,22 +889,26 @@ sub render_communication_status { my $discussionHTML = ""; my $feedbackHTML = ""; my $errorHTML = ""; my $link = $params->{"resourceLink"}; - my $linkopen = ""; + my $target; + if ($env{'environment.remotenavmap'} eq 'on') { + $target=' target="loncapaclient" '; + } + my $linkopen = ""; my $linkclose = ""; my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($resource->hasDiscussion()) { $discussionHTML = $linkopen . - '' . + ''.&mt('New Discussion').'' . $linkclose; } if ($resource->getFeedback()) { my $feedback = $resource->getFeedback(); - foreach (split(/\,/, $feedback)) { - if ($_) { - $feedbackHTML .= ' ' - . '' + . ''.&mt('New Email').''; } } @@ -1183,13 +917,13 @@ sub render_communication_status { if ($resource->getErrors()) { my $errors = $resource->getErrors(); my $errorcount = 0; - foreach (split(/,/, $errors)) { + foreach my $msgid (split(/,/, $errors)) { last if ($errorcount>=10); # Only output 10 bombs maximum - if ($_) { + if ($msgid) { $errorcount++; - $errorHTML .= ' ' - . '' + . ''.&mt('New Error').''; } } @@ -1199,7 +933,7 @@ sub render_communication_status { $discussionHTML = $feedbackHTML = $errorHTML = ''; } - return "$discussionHTML$feedbackHTML$errorHTML "; + return "$discussionHTML$feedbackHTML$errorHTML "; } sub render_quick_status { @@ -1209,7 +943,11 @@ sub render_quick_status { $params->{'multipart'} && $part eq "0"; my $link = $params->{"resourceLink"}; - my $linkopen = ""; + my $target; + if ($env{'environment.remotenavmap'} eq 'on') { + $target=' target="loncapaclient" '; + } + my $linkopen = ""; my $linkclose = ""; if ($resource->is_problem() && @@ -1220,7 +958,7 @@ sub render_quick_status { if ($icon) { my $location= &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); - $result .= "$linkopen$alt$linkclose\n"; + $result .= "$linkopen$alt$linkclose\n"; } else { $result .= " \n"; } @@ -1232,7 +970,7 @@ sub render_quick_status { } sub render_long_status { my ($resource, $part, $params) = @_; - my $result = "\n"; + my $result = "\n"; my $firstDisplayed = !$params->{'condensed'} && $params->{'multipart'} && $part eq "0"; @@ -1368,7 +1106,6 @@ sub render { # no columns, no nav maps. return ''; } - my $mustCloseNavMap = 0; my $navmap; if (defined($args->{'navmap'})) { $navmap = $args->{'navmap'}; @@ -1389,9 +1126,9 @@ sub render { # marker my $filterHash = {}; # Figure out what we're not displaying - foreach (split(/\,/, $ENV{"form.filter"})) { - if ($_) { - $filterHash->{$_} = "1"; + foreach my $item (split(/\,/, $env{"form.filter"})) { + if ($item) { + $filterHash->{$item} = "1"; } } @@ -1409,30 +1146,43 @@ sub render { } my $condition = 0; - if ($ENV{'form.condition'}) { + if ($env{'form.condition'}) { $condition = 1; } - if (!$ENV{'form.folderManip'} && !defined($args->{'iterator'})) { + if (!$env{'form.folderManip'} && !defined($args->{'iterator'})) { # Step 1: Check to see if we have a navmap if (!defined($navmap)) { $navmap = Apache::lonnavmaps::navmap->new(); - $mustCloseNavMap = 1; - } + if (!defined($navmap)) { + # no londer in course + return ''.&mt('No course selected').'
+ '.&mt('Select a course').'
'; + } + } # Step two: Locate what kind of here marker is necessary # Determine where the "here" marker is and where the screen jumps to. - if ($ENV{'form.postsymb'}) { - $here = $jump = &Apache::lonnet::symbclean($ENV{'form.postsymb'}); - } elsif ($ENV{'form.postdata'}) { + if ($env{'form.postsymb'} ne '') { + $here = $jump = &Apache::lonnet::symbclean($env{'form.postsymb'}); + } elsif ($env{'form.postdata'} ne '') { # couldn't find a symb, is there a URL? - my $currenturl = $ENV{'form.postdata'}; + my $currenturl = $env{'form.postdata'}; #$currenturl=~s/^http\:\/\///; #$currenturl=~s/^[^\/]+//; $here = $jump = &Apache::lonnet::symbread($currenturl); - } + } + if ($here eq '') { + my $last; + if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db', + &GDBM_READER(),0640)) { + $last=$hash{'last_known'}; + untie(%hash); + } + if ($last) { $here = $jump = $last; } + } # Step three: Ensure the folders are open my $mapIterator = $navmap->getIterator(undef, undef, undef, 1); @@ -1459,21 +1209,20 @@ sub render { } } - if ( !defined($args->{'iterator'}) && $ENV{'form.folderManip'} ) { # we came from a user's manipulation of the nav page + if ( !defined($args->{'iterator'}) && $env{'form.folderManip'} ) { # we came from a user's manipulation of the nav page # If this is a click on a folder or something, we want to preserve the "here" # from the querystring, and get the new "jump" marker - $here = $ENV{'form.here'}; - $jump = $ENV{'form.jump'}; + $here = $env{'form.here'}; + $jump = $env{'form.jump'}; } my $it = $args->{'iterator'}; if (!defined($it)) { - # Construct a default iterator based on $ENV{'form.'} information + # Construct a default iterator based on $env{'form.'} information # Step 1: Check to see if we have a navmap if (!defined($navmap)) { $navmap = Apache::lonnavmaps::navmap->new(); - $mustCloseNavMap = 1; } # See if we're being passed a specific map @@ -1545,13 +1294,13 @@ sub render { if ($printCloseAll && !$args->{'resource_no_folder_link'}) { my ($link,$text); if ($condition) { - $link='"navmaps?condition=0&filter=&'.$queryString. - '&here='.&Apache::lonnet::escape($here).'"'; - $text='Close All Folders'; + $link='"navmaps?condition=0&filter=&'.$queryString. + '&here='.&escape($here).'"'; + $text='Close all folders'; } else { - $link='"navmaps?condition=1&filter=&'.$queryString. - '&here='.&Apache::lonnet::escape($here).'"'; - $text='Open All Folders'; + $link='"navmaps?condition=1&filter=&'.$queryString. + '&here='.&escape($here).'"'; + $text='Open all folders'; } if ($args->{'caller'} eq 'navmapsdisplay') { &add_linkitem($args->{'linkitems'},'changefolder', @@ -1579,13 +1328,7 @@ END 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.':'; + $haveDisc .= $resource->wrap_symb().':'; $totdisc ++; } } @@ -1602,15 +1345,15 @@ END if ($args->{'caller'} eq 'navmapsdisplay') { $result .= ''; - if ($ENV{'environment.remotenavmap'} ne 'on') { + &Apache::loncommon::help_open_menu('Navigation Screen','Navigation_Screen',undef,'RAT').''; + if ($env{'environment.remotenavmap'} ne 'on') { $result .= ''; } else { $result .= ''; } $result.=&show_linkitems($args->{'linkitems'}); if ($args->{'sort_html'}) { - if ($ENV{'environment.remotenavmap'} ne 'on') { + if ($env{'environment.remotenavmap'} ne 'on') { $result.=''. ''; } else { @@ -1647,7 +1390,7 @@ END $args->{'condensed'} = 0; my $location= &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + $args->{'indentString'} = setDefault($args->{'indentString'}, "  "); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1800,10 +1543,6 @@ END $args->{'multipart'} = $curRes->multipart(); if ($condenseParts) { # do the condensation - if (!$curRes->opendate("0")) { - @parts = (); - $args->{'condensed'} = 1; - } if (!$args->{'condensed'}) { # Decide whether to condense based on similarity my $status = $curRes->status($parts[0]); @@ -1871,7 +1610,7 @@ END my $srcHasQuestion = $src =~ /\?/; $args->{"resourceLink"} = $src. ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($symb).$anchor; + 'symb=' . &escape($symb).$anchor; } # Now, we've decided what parts to show. Loop through them and # show them. @@ -2011,15 +1750,16 @@ In order of increasing complexity and po =over 4 -=item * C<$navmap-EgetByX>, where X is B, B, B or B. This provides +=item * C<$navmap-EgetByX>, where X is B, B or B and getResourceByUrl. This provides various ways to obtain resource objects, based on various identifiers. Use this when you want to request information about one object or a handful of resources you already know the identities of, from some other source. For more about Ids, Symbs, and MapPcs, see the Resource documentation. Note that Url should be a B, - not your first choice; it only works when there is only one + not your first choice; it only really works when there is only one instance of the resource in the course, which only applies to - maps, and even that may change in the future. + maps, and even that may change in the future (see the B + documentation for more details.) =item * CretrieveResources(args)>. This retrieves resources matching some criterion and returns them @@ -2061,6 +1801,7 @@ See iterator documentation below. use strict; use GDBM_File; +use Apache::lonnet; sub new { # magic invocation to create a class instance @@ -2080,7 +1821,7 @@ sub new { my %navmaphash; my %parmhash; - my $courseFn = $ENV{"request.course.fn"}; + my $courseFn = $env{"request.course.fn"}; if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db", &GDBM_READER(), 0640))) { return undef; @@ -2106,56 +1847,36 @@ sub generate_course_user_opt { my $self = shift; if ($self->{COURSE_USER_OPT_GENERATED}) { return; } - my $uname=$ENV{'user.name'}; - my $udom=$ENV{'user.domain'}; - my $uhome=$ENV{'user.home'}; - my $cid=$ENV{'request.course.id'}; - my $chome=$ENV{'course.'.$cid.'.home'}; - my ($cdom,$cnum)=split(/\_/,$cid); - - my $userprefix=$uname.'_'.$udom.'_'; + my $uname=$env{'user.name'}; + my $udom=$env{'user.domain'}; + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; - my %courserdatas; my %useropt; my %courseopt; my %userrdatas; - unless ($uhome eq 'no_host') { # ------------------------------------------------- Get coursedata (if present) - unless ((time-$courserdatas{$cid.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum. - ':resourcedata',$chome); - # Check for network failure - if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) { - $self->{NETWORK_FAILURE} = 1; - } elsif ($reply!~/^error\:/) { - $courserdatas{$cid}=$reply; - $courserdatas{$cid.'.last_cache'}=time; - } - } - foreach (split(/\&/,$courserdatas{$cid})) { - my ($name,$value)=split(/\=/,$_); - $courseopt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); + my $courseopt=&Apache::lonnet::get_courseresdata($cnum,$cdom); + # Check for network failure + if (!ref($courseopt)) { + if ( $courseopt =~ /no.such.host/i || $courseopt =~ /con_lost/i) { + $self->{NETWORK_FAILURE} = 1; } + undef($courseopt); + } + # --------------------------------------------------- Get userdata (if present) - unless ((time-$userrdatas{$uname.'___'.$udom.'.last_cache'})<240) { - my $reply=&Apache::lonnet::reply('dump:'.$udom.':'.$uname.':resourcedata',$uhome); - if ($reply!~/^error\:/) { - $userrdatas{$uname.'___'.$udom}=$reply; - $userrdatas{$uname.'___'.$udom.'.last_cache'}=time; - } - # check to see if network failed - elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) - { - $self->{NETWORK_FAILURE} = 1; - } - } - foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { - my ($name,$value)=split(/\=/,$_); - $useropt{$userprefix.&Apache::lonnet::unescape($name)}= - &Apache::lonnet::unescape($value); + + my $useropt=&Apache::lonnet::get_userresdata($uname,$udom); + # Check for network failure + if (!ref($useropt)) { + if ( $useropt =~ /no.such.host/i || $useropt =~ /con_lost/i) { + $self->{NETWORK_FAILURE} = 1; } - $self->{COURSE_OPT} = \%courseopt; - $self->{USER_OPT} = \%useropt; + undef($useropt); } + $self->{COURSE_OPT} = $courseopt; + $self->{USER_OPT} = $useropt; + $self->{COURSE_USER_OPT_GENERATED} = 1; return; @@ -2166,51 +1887,59 @@ sub generate_email_discuss_status { my $symb = shift; if ($self->{EMAIL_DISCUSS_GENERATED}) { return; } - my $cid=$ENV{'request.course.id'}; - my ($cdom,$cnum)=split(/\_/,$cid); + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; my %emailstatus = &Apache::lonnet::dump('email_status'); my $logoutTime = $emailstatus{'logout'}; - my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; + my $courseLeaveTime = $emailstatus{'logout_'.$env{'request.course.id'}}; $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? $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'); + $env{'user.domain'},$env{'user.name'},'lastread'); my %lastreadtime = (); - foreach (keys %lastread) { - my $key = $_; - $key =~ s/_lastread$//; - $lastreadtime{$key} = $lastread{$_}; + foreach my $key (keys %lastread) { + my $shortkey = $key; + $shortkey =~ s/_lastread$//; + $lastreadtime{$shortkey} = $lastread{$key}; } my %feedback=(); my %error=(); - my $keys = &Apache::lonnet::reply('keys:'. - $ENV{'user.domain'}.':'. - $ENV{'user.name'}.':nohist_email', - $ENV{'user.home'}); + my @keys = &Apache::lonnet::getkeys('nohist_email',$env{'user.domain'}, + $env{'user.name'}); - foreach my $msgid (split(/\&/, $keys)) { - $msgid=&Apache::lonnet::unescape($msgid); + foreach my $msgid (@keys) { 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 { - $feedback{$url}.=','.$msgid; - } - } + my ($sendtime,$shortsubj,$fromname,$fromdomain,$status,$fromcid, + $symb,$error) = &Apache::lonmsg::unpackmsgid($msgid); + if (defined($symb)) { + if (defined($error) && $error == 1) { + $error{$symb}.=','.$msgid; + } else { + $feedback{$symb}.=','.$msgid; + } + } else { + my $plain= + &LONCAPA::unescape(&LONCAPA::unescape($msgid)); + if ($plain=~/ \[([^\]]+)\]\:/) { + my $url=$1; + if ($plain=~/\:Error \[/) { + $error{$url}.=','.$msgid; + } else { + $feedback{$url}.=','.$msgid; + } + } + } } } - #url's of resources that have feedbacks + #symbs of resources that have feedbacks (will be urls pre-2.3) $self->{FEEDBACK} = \%feedback; - #or errors + #or errors (will be urls pre 2.3) $self->{ERROR_MSG} = \%error; $self->{DISCUSSION_TIME} = \%discussiontime; $self->{EMAIL_STATUS} = \%emailstatus; @@ -2224,14 +1953,35 @@ sub get_user_data { if ($self->{RETRIEVED_USER_DATA}) { return; } # Retrieve performance data on problems - my %student_data = Apache::lonnet::currentdump($ENV{'request.course.id'}, - $ENV{'user.domain'}, - $ENV{'user.name'}); + my %student_data = Apache::lonnet::currentdump($env{'request.course.id'}, + $env{'user.domain'}, + $env{'user.name'}); $self->{STUDENT_DATA} = \%student_data; $self->{RETRIEVED_USER_DATA} = 1; } +sub get_discussion_data { + my $self = shift; + if ($self->{RETRIEVED_DISCUSSION_DATA}) { + return $self->{DISCUSSION_DATA}; + } + + $self->generate_email_discuss_status(); + + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; + # Retrieve discussion data for resources in course + my %discussion_data = &Apache::lonnet::dumpstore($cid,$cdom,$cnum); + + + $self->{DISCUSSION_DATA} = \%discussion_data; + $self->{RETRIEVED_DISCUSSION_DATA} = 1; + return $self->{DISCUSSION_DATA}; +} + + # Internal function: Takes a key to look up in the nav hash and implements internal # memory caching of that key. sub navhash { @@ -2251,7 +2001,7 @@ sub navhash { # Checks to see if coursemap is defined, matching test in old lonnavmaps sub courseMapDefined { my $self = shift; - my $uri = &Apache::lonnet::clutter($ENV{'request.course.uri'}); + my $uri = &Apache::lonnet::clutter($env{'request.course.uri'}); my $firstres = $self->navhash("map_start_$uri"); my $lastres = $self->navhash("map_finish_$uri"); @@ -2271,21 +2021,14 @@ sub getIterator { sub hasDiscussion { my $self = shift; my $symb = shift; - $self->generate_email_discuss_status(); if (!defined($self->{DISCUSSION_TIME})) { return 0; } #return defined($self->{DISCUSSION_TIME}->{$symb}); -# 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'; - } - } - + # backward compatibility (bulletin boards used to be 'wrapped') + my $ressymb = $self->wrap_symb($symb); if ( defined ( $self->{LAST_READ}->{$ressymb} ) ) { return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_READ}->{$ressymb}; } else { @@ -2294,29 +2037,137 @@ sub hasDiscussion { } } +sub last_post_time { + my $self = shift; + my $symb = shift; + my $ressymb = $self->wrap_symb($symb); + return $self->{DISCUSSION_TIME}->{$ressymb}; +} + +sub discussion_info { + my $self = shift; + my $symb = shift; + my $filter = shift; + + $self->get_discussion_data(); + + my $ressymb = $self->wrap_symb($symb); + # keys used to store bulletinboard postings use 'unwrapped' symb. + my $discsymb = $self->unwrap_symb($ressymb); + my $version = $self->{DISCUSSION_DATA}{'version:'.$discsymb}; + if (!$version) { return; } + + my $prevread = $self->{LAST_READ}{$ressymb}; + + my $count = 0; + my $hiddenflag = 0; + my $deletedflag = 0; + my ($hidden,$deleted,%info); + + for (my $id=$version; $id>0; $id--) { + my $vkeys=$self->{DISCUSSION_DATA}{$id.':keys:'.$discsymb}; + my @keys=split(/:/,$vkeys); + if (grep(/^hidden$/ ,@keys)) { + if (!$hiddenflag) { + $hidden = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':hidden'}; + $hiddenflag = 1; + } + } elsif (grep(/^deleted$/,@keys)) { + if (!$deletedflag) { + $deleted = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':deleted'}; + $deletedflag = 1; + } + } else { + if (($hidden !~/\.$id\./) && ($deleted !~/\.$id\./)) { + if ($filter eq 'unread') { + if ($prevread >= $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':timestamp'}) { + next; + } + } + $count++; + $info{$count}{'subject'} = + $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':subject'}; + $info{$count}{'id'} = $id; + $info{$count}{'timestamp'} = $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':timestamp'}; + } + } + } + if (wantarray) { + return ($count,%info); + } + return $count; +} + +sub wrap_symb { + my $self = shift; + my $symb = shift; + if ($symb =~ m-___(adm/[^/]+/[^/]+/)(\d+)(/bulletinboard)$-) { + unless ($symb =~ m|adm/wrapper/adm|) { + $symb = 'bulletin___'.$2.'___adm/wrapper/'.$1.$2.$3; + } + } + return $symb; +} + +sub unwrap_symb { + my $self = shift; + my $ressymb = shift; + my $discsymb = $ressymb; + if ($ressymb =~ m-^(bulletin___\d+___)adm/wrapper/(adm/[^/]+/[^/]+/\d+/bulletinboard)$-) { + $discsymb = $1.$2; + } + return $discsymb; +} + # Private method: Does the given resource (as a symb string) have # current feedback? Returns the string in the feedback hash, which # will be false if it does not exist. + sub getFeedback { my $self = shift; my $symb = shift; + my $source = shift; $self->generate_email_discuss_status(); if (!defined($self->{FEEDBACK})) { return ""; } - return $self->{FEEDBACK}->{$symb}; + my $feedback; + if ($self->{FEEDBACK}->{$symb}) { + $feedback = $self->{FEEDBACK}->{$symb}; + if ($self->{FEEDBACK}->{$source}) { + $feedback .= ','.$self->{FEEDBACK}->{$source}; + } + } else { + if ($self->{FEEDBACK}->{$source}) { + $feedback = $self->{FEEDBACK}->{$source}; + } + } + return $feedback; } # Private method: Get the errors for that resource (by source). sub getErrors { my $self = shift; + my $symb = shift; my $src = shift; $self->generate_email_discuss_status(); if (!defined($self->{ERROR_MSG})) { return ""; } - return $self->{ERROR_MSG}->{$src}; + + my $errors; + if ($self->{ERROR_MSG}->{$symb}) { + $errors = $self->{ERROR_MSG}->{$symb}; + if ($self->{ERROR_MSG}->{$src}) { + $errors .= ','.$self->{ERROR_MSG}->{$src}; + } + } else { + if ($self->{ERROR_MSG}->{$src}) { + $errors = $self->{ERROR_MSG}->{$src}; + } + } + return $errors; } =pod @@ -2392,7 +2243,7 @@ resource in the navmap. sub firstResource { my $self = shift; my $firstResource = $self->navhash('map_start_' . - &Apache::lonnet::clutter($ENV{'request.course.uri'})); + &Apache::lonnet::clutter($env{'request.course.uri'})); return $self->getById($firstResource); } @@ -2408,7 +2259,7 @@ in the navmap. sub finishResource { my $self = shift; my $firstResource = $self->navhash('map_finish_' . - &Apache::lonnet::clutter($ENV{'request.course.uri'})); + &Apache::lonnet::clutter($env{'request.course.uri'})); return $self->getById($firstResource); } @@ -2435,16 +2286,22 @@ sub parmval_real { # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated $self->generate_course_user_opt(); - my $cid=$ENV{'request.course.id'}; - my $csec=$ENV{'request.course.sec'}; - my $uname=$ENV{'user.name'}; - my $udom=$ENV{'user.domain'}; + my $cid=$env{'request.course.id'}; + my $csec=$env{'request.course.sec'}; + my $cgroup=''; + my @cgrps=split(/:/,$env{'request.course.groups'}); + if (@cgrps > 0) { + @cgrps = sort(@cgrps); + $cgroup = $cgrps[0]; + } + my $uname=$env{'user.name'}; + my $udom=$env{'user.domain'}; unless ($symb) { return ''; } my $result=''; my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); - + $mapname = &Apache::lonnet::deversion($mapname); # ----------------------------------------------------- Cascading lookup scheme my $rwhat=$what; $what=~s/^parameter\_//; @@ -2452,7 +2309,11 @@ sub parmval_real { my $symbparm=$symb.'.'.$what; my $mapparm=$mapname.'___(all).'.$what; - my $usercourseprefix=$uname.'_'.$udom.'_'.$cid; + my $usercourseprefix=$cid; + + my $grplevel=$usercourseprefix.'.['.$cgroup.'].'.$what; + my $grplevelr=$usercourseprefix.'.['.$cgroup.'].'.$symbparm; + my $grplevelm=$usercourseprefix.'.['.$cgroup.'].'.$mapparm; my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what; my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm; @@ -2474,6 +2335,12 @@ sub parmval_real { } # ------------------------------------------------------- second, check course + if ($cgroup ne '' and defined($courseopt)) { + if (defined($$courseopt{$grplevelr})) { return $$courseopt{$grplevelr}; } + if (defined($$courseopt{$grplevelm})) { return $$courseopt{$grplevelm}; } + if (defined($$courseopt{$grplevel})) { return $$courseopt{$grplevel}; } + } + if ($csec and defined($courseopt)) { if (defined($$courseopt{$seclevelr})) { return $$courseopt{$seclevelr}; } if (defined($$courseopt{$seclevelm})) { return $$courseopt{$seclevelm}; } @@ -2524,14 +2391,17 @@ sub parmval_real { =pod -=item * B(url): +=item * B(url,multiple): -Retrieves a resource object by URL of the resource. If passed a -resource object, it will simply return it, so it is safe to use this -method in code like "$res = $navmap->getResourceByUrl($res)", if -you're not sure if $res is already an object, or just a URL. If the -resource appears multiple times in the course, only the first instance -will be returned. As a result, this is probably useful only for maps. +Retrieves a resource object by URL of the resource, unless the optional +multiple parameter is included in wahich caes an array of resource +objects is returned. If passed a resource object, it will simply return +it, so it is safe to use this method in code like +"$res = $navmap->getResourceByUrl($res)" +if you're not sure if $res is already an object, or just a URL. If the +resource appears multiple times in the course, only the first instance +will be returned (useful for maps), unless the multiple parameter has +been included, in which case all instances are returned in an array. =item * B(map, filterFunc, recursive, bailout, showall): @@ -2557,31 +2427,50 @@ want to know is if I resources matc parameter will allow you to avoid potentially expensive enumeration of all matching resources. -=item * B(map, filterFunc, recursive): +=item * B(map, filterFunc, recursive, showall): Convience method for - scalar(retrieveResources($map, $filterFunc, $recursive, 1)) > 0 + scalar(retrieveResources($map, $filterFunc, $recursive, 1, $showall)) > 0 which will tell whether the map has resources matching the description in the filter function. +=item * B(url): + +Retrieves version infomation for a url. Returns the version (a number, or +the string "mostrecent") for resources which have version information in +the big hash. + =cut sub getResourceByUrl { my $self = shift; my $resUrl = shift; + my $multiple = shift; if (ref($resUrl)) { return $resUrl; } $resUrl = &Apache::lonnet::clutter($resUrl); my $resId = $self->{NAV_HASH}->{'ids_' . $resUrl}; - if ($resId =~ /,/) { - $resId = (split (/,/, $resId))[0]; - } if (!$resId) { return ''; } - return $self->getById($resId); + if ($multiple) { + my @resources = (); + my @resIds = split (/,/, $resId); + foreach my $id (@resIds) { + my $resourceId = $self->getById($id); + if ($resourceId) { + push(@resources,$resourceId); + } + } + return @resources; + } else { + if ($resId =~ /,/) { + $resId = (split (/,/, $resId))[0]; + } + return $self->getById($resId); + } } sub retrieveResources { @@ -2644,14 +2533,23 @@ sub hasResource { my $map = shift; my $filterFunc = shift; my $recursive = shift; + my $showall = shift; - return scalar($self->retrieveResources($map, $filterFunc, $recursive, 1)) > 0; + return scalar($self->retrieveResources($map, $filterFunc, $recursive, 1, $showall)) > 0; +} + +sub usedVersion { + my $self = shift; + my $linkurl = shift; + return $self->navhash("version_$linkurl"); } 1; package Apache::lonnavmaps::iterator; -use WeakRef; +use Scalar::Util qw(weaken); +use Apache::lonnet; + =pod =back @@ -3130,7 +3028,9 @@ sub populateStack { 1; package Apache::lonnavmaps::DFSiterator; -use WeakRef; +use Scalar::Util qw(weaken); +use Apache::lonnet; + # 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 @@ -3257,9 +3157,9 @@ sub next { # filter the next possibilities to remove things we've # already seen. - foreach (@$nextUnfiltered) { - if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { - push @$next, $_; + foreach my $item (@$nextUnfiltered) { + if (!defined($self->{ALREADY_SEEN}->{$item->{ID}})) { + push @$next, $item; } } @@ -3312,7 +3212,7 @@ sub populateStack { 1; package Apache::lonnavmaps::resource; -use WeakRef; +use Scalar::Util qw(weaken); use Apache::lonnet; =pod @@ -3491,8 +3391,7 @@ sub kind { my $self=shift; return $self- sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } sub randompick { my $self = shift; - return $self->{NAV_MAP}->{PARM_HASH}->{$self->symb . - '.0.parameter_randompick'}; + return $self->parmval('randompick'); } sub link { my $self=shift; @@ -3508,6 +3407,15 @@ sub shown_symb { if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->symb());} return $self->symb(); } +sub id { + my $self=shift; + return $self->{ID}; +} +sub enclosing_map_src { + my $self=shift; + (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; + return $self->navHash('map_id_'.$first); +} sub symb { my $self=shift; (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; @@ -3516,12 +3424,16 @@ sub symb { . '___' . $second . '___' . $symbSrc; return &Apache::lonnet::symbclean($symb); } +sub wrap_symb { + my $self = shift; + return $self->{NAV_MAP}->wrap_symb($self->symb()); +} sub title { my $self=shift; if ($self->{ID} eq '0.0') { # If this is the top-level map, return the title of the course # since this map can not be titled otherwise. - return $ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + return $env{'course.'.$env{'request.course.id'}.'.description'}; } return $self->navHash("title_", 1); } # considered private and undocumented @@ -3535,7 +3447,18 @@ sub condition { my $condition=&Apache::lonnet::directcondval($condid); return $condition; } +sub condval { + my $self=shift; + my ($pathname,$filename) = + &Apache::lonnet::split_uri_for_cond($self->src()); + my $match=($env{'acc.res.'.$env{'request.course.id'}.'.'.$pathname}=~ + /\&\Q$filename\E\:([\d\|]+)\&/); + if ($match) { + return &Apache::lonnet::condval($1); + } + return 0; +} sub compTitle { my $self = shift; my $title = $self->title(); @@ -3586,6 +3509,16 @@ sub retrieveResources { return $self->{NAV_MAP}->retrieveResources(@_); } +sub is_exam { + my ($self,$part) = @_; + if ($self->parmval('type',$part) eq 'exam') { + return 1; + } + if ($self->src() =~ /\.(exam)$/) { + return 1; + } + return 0; +} sub is_html { my $self=shift; my $src = $self->src(); @@ -3598,10 +3531,21 @@ sub is_page { return $self->navHash("is_map_", 1) && $self->navHash("map_type_" . $self->map_pc()) eq 'page'; } +sub is_practice { + my $self=shift; + my ($part) = @_; + if ($self->parmval('type',$part) eq 'practice') { + return 1; + } + return 0; +} sub is_problem { my $self=shift; my $src = $self->src(); - return ($src =~ /\.(problem|exam|quiz|assess|survey|form|library)$/) + if ($src =~ /\.(problem|exam|quiz|assess|survey|form|library|task)$/) { + return !($self->is_practice()); + } + return 0; } sub contains_problem { my $self=shift; @@ -3613,7 +3557,6 @@ sub contains_problem { } sub is_sequence { my $self=shift; - my $src = $self->src(); return $self->navHash("is_map_", 1) && $self->navHash("map_type_" . $self->map_pc()) eq 'sequence'; } @@ -3628,6 +3571,11 @@ sub is_survey { } return 0; } +sub is_task { + my $self=shift; + my $src = $self->src(); + return ($src =~ /\.(task)$/) +} sub is_empty_sequence { my $self=shift; @@ -3683,7 +3631,7 @@ Returns a string with the type of the ma sub map_finish { my $self = shift; my $src = $self->src(); - $src = Apache::lonnet::clutter($src); + $src = &Apache::lonnet::clutter($src); my $res = $self->navHash("map_finish_$src", 0); $res = $self->{NAV_MAP}->getById($res); return $res; @@ -3696,7 +3644,7 @@ sub map_pc { sub map_start { my $self = shift; my $src = $self->src(); - $src = Apache::lonnet::clutter($src); + $src = &Apache::lonnet::clutter($src); my $res = $self->navHash("map_start_$src", 0); $res = $self->{NAV_MAP}->getById($res); return $res; @@ -3808,14 +3756,28 @@ sub awarded { if (!defined($part)) { $part = '0'; } return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.awarded'}; } +# this should work exactly like the copy in lonhomework.pm sub duedate { (my $self, my $part) = @_; + my $date; my $interval=$self->parmval("interval", $part); - if ($interval) { + my $due_date=$self->parmval("duedate", $part); + if ($interval =~ /\d+/) { my $first_access=&Apache::lonnet::get_first_access('map',$self->symb); - if ($first_access) { return ($first_access+$interval); } + if (defined($first_access)) { + $interval = $first_access+$interval; + $date = ($interval < $due_date)? $interval : $due_date; + } else { + $date = $due_date; + } + } else { + $date = $due_date; } - return $self->parmval("duedate", $part); + return $date; +} +sub handgrade { + (my $self, my $part) = @_; + return $self->parmval("handgrade", $part); } sub maxtries { (my $self, my $part) = @_; @@ -3855,9 +3817,9 @@ sub weight { my $self = shift; my $part = shift; if (!defined($part)) { $part = '0'; } return &Apache::lonnet::EXT('resource.'.$part.'.weight', - $self->symb(), $ENV{'user.domain'}, - $ENV{'user.name'}, - $ENV{'request.course.sec'}); + $self->symb(), $env{'user.domain'}, + $env{'user.name'}, + $env{'request.course.sec'}); } sub part_display { my $self= shift(); my $partID = shift(); @@ -3908,6 +3870,21 @@ Returns a false value if there has been logged in, true if there has. Always returns false if the discussion data was not extracted when the nav map was constructed. +=item * B: + +Returns a false value if there hasn't been discussion otherwise returns +unix timestamp of last time a discussion posting (or edit) was made. + +=item * B: + +optional argument is a filter (currently can be 'unread'); +returns in scalar context the count of the number of discussion postings. + +returns in list context both the count of postings and a hash ref +containing information about the postings (subject, id, timestamp) in a hash. + +Default is to return counts for all postings. However if called with a second argument set to 'unread', will return information about only unread postings. + =item * B: Gets the feedback for the resource and returns the raw feedback string @@ -3915,8 +3892,8 @@ for the resource, or the null string if email data was not extracted when the nav map was constructed. Usually used like this: - for (split(/\,/, $res->getFeedback())) { - my $link = &Apache::lonnet::escape($_); + for my $url (split(/\,/, $res->getFeedback())) { + my $link = &escape($url); ... and use the link as appropriate. @@ -3928,18 +3905,30 @@ sub hasDiscussion { return $self->{NAV_MAP}->hasDiscussion($self->symb()); } +sub last_post_time { + my $self = shift; + return $self->{NAV_MAP}->last_post_time($self->symb()); +} + +sub discussion_info { + my ($self,$filter) = @_; + return $self->{NAV_MAP}->discussion_info($self->symb(),$filter); +} + sub getFeedback { my $self = shift; my $source = $self->src(); + my $symb = $self->symb(); if ($source =~ /^\/res\//) { $source = substr $source, 5; } - return $self->{NAV_MAP}->getFeedback($source); + return $self->{NAV_MAP}->getFeedback($symb,$source); } sub getErrors { my $self = shift; my $source = $self->src(); + my $symb = $self->symb(); if ($source =~ /^\/res\//) { $source = substr $source, 5; } - return $self->{NAV_MAP}->getErrors($source); + return $self->{NAV_MAP}->getErrors($symb,$source); } =pod @@ -4025,7 +4014,7 @@ sub countResponses { sub responseTypes { my $self = shift; my %responses; - foreach my $part ($self->parts()) { + foreach my $part (@{$self->parts()}) { foreach my $responsetype ($self->responseType($part)) { $responses{$responsetype}++ if (defined($responsetype)); } @@ -4100,8 +4089,8 @@ sub extractParts { $self->{PART_TYPE} = {}; return; } - foreach (split(/\,/,$metadata)) { - if ($_ =~ /^part_(.*)$/) { + foreach my $entry (split(/\,/,$metadata)) { + if ($entry =~ /^(?:part|Task)_(.*)$/) { my $part = $1; # This floods the logs if it blows up if (defined($parts{$part})) { @@ -4126,8 +4115,8 @@ sub extractParts { # Init the responseIdHash - foreach (@{$self->{PARTS}}) { - $responseIdHash{$_} = []; + foreach my $part (@{$self->{PARTS}}) { + $responseIdHash{$part} = []; } # Now, the unfortunate thing about this is that parts, part name, and @@ -4136,8 +4125,9 @@ sub extractParts { # So we have to use our knowlege of part names to figure out # where the part names begin and end, and even then, it is possible # to construct ambiguous situations. - foreach (split /,/, $metadata) { - if ($_ =~ /^([a-zA-Z]+)response_(.*)/) { + foreach my $data (split /,/, $metadata) { + if ($data =~ /^([a-zA-Z]+)response_(.*)/ + || $data =~ /^(Task)_(.*)/) { my $responseType = $1; my $partStuff = $2; my $partIdSoFar = ''; @@ -4149,8 +4139,15 @@ sub extractParts { if ($parts{$partIdSoFar}) { my @otherChunks = @partChunks[$i+1..$#partChunks]; my $responseId = join('_', @otherChunks); - push @{$responseIdHash{$partIdSoFar}}, $responseId; - push @{$responseTypeHash{$partIdSoFar}}, $responseType; + if ($self->is_task()) { + push(@{$responseIdHash{$partIdSoFar}}, + $partIdSoFar); + } else { + push(@{$responseIdHash{$partIdSoFar}}, + $responseId); + } + push(@{$responseTypeHash{$partIdSoFar}}, + $responseType); } } } @@ -4199,13 +4196,13 @@ the completion information. Idiomatic usage of these two methods would probably look something like - foreach ($resource->parts()) { - my $dateStatus = $resource->getDateStatus($_); - my $completionStatus = $resource->getCompletionStatus($_); + foreach my $part ($resource->parts()) { + my $dateStatus = $resource->getDateStatus($part); + my $completionStatus = $resource->getCompletionStatus($part); or - my $status = $resource->status($_); + my $status = $resource->status($part); ... use it here ... } @@ -4366,14 +4363,17 @@ sub ATTEMPTED { return 16; } sub getCompletionStatus { my $self = shift; + my $part = shift; return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE}); - my $status = $self->queryRestoreHash('solved', shift); + my $status = $self->queryRestoreHash('solved', $part); # 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 '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; } if ($status eq 'excused') {return $self->EXCUSED; } @@ -4477,6 +4477,7 @@ An answer has been submitted, but the st sub TRIES_LEFT { return 20; } sub ANSWER_SUBMITTED { return 21; } +sub PARTIALLY_CORRECT{ return 22; } sub status { my $self = shift; @@ -4495,14 +4496,29 @@ sub status { my $suppressFeedback = $self->problemstatus($part) eq 'no'; # If there's an answer date and we're past it, don't # suppress the feedback; student should know - if ($self->answerdate($part) && $self->answerdate($part) < time()) { + if ($self->duedate($part) && $self->duedate($part) < time() && + $self->answerdate($part) && $self->answerdate($part) < time()) { $suppressFeedback = 0; } # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || $completionStatus == CORRECT_BY_OVERRIDE ) { - return $suppressFeedback? ANSWER_SUBMITTED : CORRECT; + if ( $suppressFeedback ) { return ANSWER_SUBMITTED } + my $awarded=$self->awarded($part); + if ($awarded < 1 && $awarded > 0) { + return PARTIALLY_CORRECT; + } elsif ($awarded<1) { + return INCORRECT; + } + return CORRECT; + } + + # If it's WRONG... and not open + if ( ($completionStatus == INCORRECT || + $completionStatus == INCORRECT_BY_OVERRIDE) + && (!$self->opendate($part) || $self->opendate($part) > time()) ) { + return INCORRECT; } if ($completionStatus == ATTEMPTED) { @@ -4589,6 +4605,7 @@ my %compositeToSimple = NETWORK_FAILURE() => ERROR, NOTHING_SET() => CLOSED, CORRECT() => CORRECT, + PARTIALLY_CORRECT() => PARTIALLY_CORRECT, EXCUSED() => CORRECT, PAST_DUE_NO_ANSWER() => INCORRECT, PAST_DUE_ANSWER_LATER() => INCORRECT, @@ -4709,7 +4726,7 @@ sub getNext { my $to = $self->to(); foreach my $branch ( split(/,/, $to) ) { my $choice = $self->{NAV_MAP}->getById($branch); - if (!$choice->condition()) { next; } + #if (!$choice->condition()) { next; } my $next = $choice->goesto(); $next = $self->{NAV_MAP}->getById($next);
'. - &Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT').' 
   '.$args->{'sort_html'}.'