--- loncom/interface/lonnavmaps.pm 2004/09/21 20:33:49 1.296 +++ loncom/interface/lonnavmaps.pm 2008/12/12 22:27:20 1.415.2.2 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.296 2004/09/21 20:33:49 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.415.2.2 2008/12/12 22:27:20 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,12 +30,15 @@ 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 used +use Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; +use DateTime(); # symbolic constants sub SYMB { return 1; } @@ -53,6 +56,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 => '' @@ -77,36 +81,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 +# is not yet done and due in less than 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'; @@ -119,26 +102,9 @@ window.status='Done.'; ENDCLOSE } -sub nav_control_js { - my $nav=($ENV{'environment.remotenavmap'} eq 'on'); - return (< @@ -150,284 +116,6 @@ this.document.navform.submit(); ENDUPDATE } -sub handler { - my $r = shift; - real_handler($r); -} - -sub real_handler { - my $r = shift; - - # 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=(); - - 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(); - - if (!defined($navmap)) { - my $requrl = $r->uri; - $ENV{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; - } - - $r->print("\n"); - $r->print("".&mt('Navigate Course Contents').""); -# ------------------------------------------------------------ Get query string - &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().';'. - $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); - $navmap->untieHashes(); - - # 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.

"); - } - } - - $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 @@ -451,7 +139,6 @@ sub removeFromFilter { # Convenience function: Given a stack returned from getStack on the iterator, # return the correct src() value. -# Later, this should add an anchor when we start putting anchors in pages. sub getLinkForResource { my $stack = shift; my $res; @@ -459,14 +146,18 @@ sub getLinkForResource { # Check to see if there are any pages in the stack foreach $res (@$stack) { if (defined($res)) { + my $anchor; if ($res->is_page()) { - return $res->src(); + 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$/) { - return &Apache::lonnet::clutter($map).'#'. - &Apache::lonnet::escape(&Apache::lonnet::declutter($src)); + my $url=&Apache::lonnet::clutter($map); + $anchor=&escape($src->shown_symb()); + return ($url,$res->shown_symb(),$anchor); } } } @@ -475,11 +166,14 @@ 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->src(); + if ($res) { + return ($res->link(),$res->shown_symb()); + } + return; } # Convenience function: This separates the logic of how to create @@ -493,6 +187,10 @@ sub getDescription { my $part = shift; my $status = $res->status($part); + my $open = $res->opendate($part); + my $due = $res->duedate($part); + my $answer = $res->answerdate($part); + if ($status == $res->NETWORK_FAILURE) { return &mt("Having technical difficulties; please check status later"); } @@ -500,22 +198,31 @@ sub getDescription { return &mt("Not currently assigned."); } if ($status == $res->OPEN_LATER) { - return "Open " . timeToHumanString($res->opendate($part)); + return &mt("Open ") .timeToHumanString($open,'start'); } if ($status == $res->OPEN) { - if ($res->duedate($part)) { - return &mt("Due")." " .timeToHumanString($res->duedate($part)); + if ($due) { + if ($res->is_practice()) { + return &mt("Closes ")." " .timeToHumanString($due,'start'); + } else { + return &mt("Due")." " .timeToHumanString($due,'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($answer,'start'); } if ($status == $res->PAST_DUE_NO_ANSWER) { - return &mt("Was due")." " . timeToHumanString($res->duedate($part)); + if ($res->is_practice()) { + return &mt("Closed")." " . timeToHumanString($due,'start'); + } else { + return &mt("Was due")." " . timeToHumanString($due,'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) { @@ -529,13 +236,13 @@ sub getDescription { my $maxtries = $res->maxtries($part); my $triesString = ""; if ($tries && $maxtries) { - $triesString = "($tries of $maxtries tries used)"; + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; if ($maxtries > 1 && $maxtries - $tries == 1) { $triesString = "$triesString"; } } - if ($res->duedate($part)) { - return &mt("Due")." " . timeToHumanString($res->duedate($part)) . + if ($due) { + return &mt("Due")." " . timeToHumanString($due,'end') . " $triesString"; } else { return &mt("No due date")." $triesString"; @@ -546,7 +253,7 @@ sub getDescription { } } -# Convenience function, so others can use it: Is the problem due in less then +# Convenience function, so others can use it: Is the problem due in less than # 24 hours, and still can be done? sub dueInLessThan24Hours { @@ -561,7 +268,7 @@ sub dueInLessThan24Hours { } # Convenience function, so others can use it: Is there only one try remaining for the -# part, with more then one try to begin with, not due yet and still can be done? +# part, with more than one try to begin with, not due yet and still can be done? sub lastTry { my $res = shift; my $part = shift; @@ -573,10 +280,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'}; } @@ -588,8 +295,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'); @@ -599,9 +309,6 @@ sub timeToHumanString { } my $now = time(); - my @time = localtime($time); - my @now = localtime($now); - # Positive = future my $delta = $time - $now; @@ -627,13 +334,13 @@ sub timeToHumanString { my $tense = $inPast ? " ago" : ""; my $prefix = $inPast ? "" : "in "; - # Less then a minute + # Less than a minute if ( $delta < $minute ) { if ($delta == 1) { return "${prefix}1 second$tense"; } return "$prefix$delta seconds$tense"; } - # Less then an hour + # Less than an hour if ( $delta < $hour ) { # If so, use minutes my $minutes = floor($delta / 60); @@ -641,7 +348,7 @@ sub timeToHumanString { return "$prefix$minutes minutes$tense"; } - # Is it less then 24 hours away? If so, + # Is it less than 24 hours away? If so, # display hours + minutes if ( $delta < $hour * 24) { my $hours = floor($delta / $hour); @@ -660,27 +367,47 @@ sub timeToHumanString { return "$prefix$hourString$minuteString$tense"; } - # Less then 5 days away, display day of the week and + my $dt = DateTime->from_epoch(epoch => $time) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + + # If there's a caller supplied format, use it. + + if ($format ne '') { + my $timeStr = $dt->strftime($format); + return $timeStr.' ('.$dt->time_zone_short_name().')'; + } + + # Less than 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)); + my $timeStr = $dt->strftime("%A, %b %e at %I:%M %P (%Z)"); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "next ") . + return ($inPast ? "last " : "this ") . $timeStr; } + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } # Is it this year? - if ( $time[5] == $now[5]) { + my $dt_now = DateTime->from_epoch(epoch => $now) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + if ( $dt->year() == $dt_now->year()) { # Return on Month Day, HH:MM meridian - my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); + my $timeStr = $dt->strftime("$conjunction %A, %b %e at %I:%M %P (%Z)"); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return $timeStr; } # Not this year, so show the year - my $timeStr = strftime("on %A, %b %e %Y at %I:%M %P", localtime($time)); + my $timeStr = + $dt->strftime("$conjunction %A, %b %e %Y at %I:%M %P (%Z)"); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; return $timeStr; @@ -718,7 +445,7 @@ to compute due to the amount of data tha processed. Apache::lonnavmaps provides an object model for manipulating this -information in a higher-level fashion then directly manipulating +information in a higher-level fashion than directly manipulating the hash. It also provides access to several auxilary functions that aren't necessarily stored in the Big Hash, but are a per- resource sort of value, like whether there is any feedback on @@ -764,7 +491,7 @@ Apache::lonnavmaps::render({}). =head2 Overview of Columns The renderer will build an HTML table for the navmap and return -it. The table is consists of several columns, and a row for each +it. The table consists of several columns, and a row for each resource (or possibly each part). You tell the renderer how many columns to create and what to place in each column, optionally using one or more of the prepared columns, and the renderer will assemble @@ -879,13 +606,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 @@ -895,11 +622,16 @@ 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: false + +If you need to include the top level map (meaning the course) in the +rendered output set this to true + +=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 @@ -925,12 +657,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 @@ -958,7 +690,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 @@ -1027,6 +759,14 @@ sub render_resource { my $nonLinkedText = ''; # stuff after resource title not in link my $link = $params->{"resourceLink"}; + + # The URL part is not escaped at this point, but the symb is... + # The stuff to the left of the ? must have ' replaced by \' since + # it will be quoted with ' in the href. + + my ($left,$right) = split(/\?/, $link); + $link = $left.'?'.$right; + my $src = $resource->src(); my $it = $params->{"iterator"}; my $filter = $it->{FILTER}; @@ -1035,28 +775,37 @@ sub render_resource { 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 = "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 @@ -1068,27 +817,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 = ""; @@ -1096,11 +849,17 @@ sub render_resource { } if ($resource->randomout()) { - $nonLinkedText .= ' (hidden) '; + $nonLinkedText .= ' ('.&mt('hidden').') '; } - + if (!$resource->condval()) { + $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; + } + if (($resource->is_practice()) && ($resource->is_raw_problem())) { + $nonLinkedText .=' '.&mt('not graded').''; + } + # We're done preparing and finally ready to start the rendering - my $result = ""; + my $result = ""; my $indentLevel = $params->{'indentLevel'}; if ($newBranchText) { $indentLevel--; } @@ -1111,6 +870,7 @@ sub render_resource { } # Decide what to display + $result .= "$newBranchText$linkopen$icon$linkclose"; my $curMarkerBegin = ''; @@ -1119,7 +879,7 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '> '; + $curMarkerBegin = '>'; $curMarkerEnd = '<'; $params->{'displayedHereMarker'} = 1; } @@ -1127,21 +887,21 @@ sub render_resource { if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { my $displaypart=$resource->part_display($part); - $partLabel = " (Part: $displaypart)"; - $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"; } @@ -1154,22 +914,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').''; } } @@ -1178,13 +942,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').''; } } @@ -1194,7 +958,7 @@ sub render_communication_status { $discussionHTML = $feedbackHTML = $errorHTML = ''; } - return "$discussionHTML$feedbackHTML$errorHTML "; + return "$discussionHTML$feedbackHTML$errorHTML "; } sub render_quick_status { @@ -1204,7 +968,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() && @@ -1213,7 +981,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"; } @@ -1225,12 +995,12 @@ 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"; my $color; - if ($resource->is_problem()) { + if ($resource->is_problem() || $resource->is_practice()) { $color = $colormap{$resource->status}; if (dueInLessThan24Hours($resource, $part) || @@ -1240,14 +1010,17 @@ sub render_long_status { } if ($resource->kind() eq "res" && - $resource->is_problem() && + ($resource->is_problem() || $resource->is_practice()) && !$firstDisplayed) { if ($color) {$result .= ""; } $result .= getDescription($resource, $part); if ($color) {$result .= ""; } } - if ($resource->is_map() && advancedUser() && $resource->randompick()) { - $result .= '(randomly select ' . $resource->randompick() .')'; + if ($resource->is_map() && &advancedUser() && $resource->randompick()) { + $result .= &mt('(randomly select [_1])', $resource->randompick()); + } + if ($resource->is_map() && &advancedUser() && $resource->randomorder()) { + $result .= &mt('(randomly ordered)'); } # Debugging code @@ -1282,7 +1055,6 @@ my %statusStrings = ); my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR); -use Data::Dumper; sub render_parts_summary_status { my ($resource, $part, $params) = @_; if (!$resource->is_problem() && !$resource->contains_problem) { return ''; } @@ -1355,14 +1127,12 @@ 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)) { # no columns, no nav maps. return ''; } - my $mustCloseNavMap = 0; my $navmap; if (defined($args->{'navmap'})) { $navmap = $args->{'navmap'}; @@ -1383,9 +1153,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"; } } @@ -1403,30 +1173,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 = $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); @@ -1453,21 +1236,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 @@ -1479,7 +1261,7 @@ sub render { $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); } else { - $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition); + $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'}); } } @@ -1512,23 +1294,23 @@ 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 .= ''; } @@ -1538,13 +1320,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', @@ -1560,9 +1342,11 @@ sub render { &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; @@ -1570,44 +1354,32 @@ 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 ++; } } if ($totdisc > 0) { $haveDisc =~ s/:$//; - my $navurl = $ENV{'QUERY_STRING'}; $result .= (< END } - } else { - my $time=time; - $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') { + &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 { @@ -1642,7 +1414,9 @@ END $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, @@ -1795,10 +1569,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]); @@ -1848,7 +1618,26 @@ END # Add part 0 so we display it correctly. unshift @parts, '0'; } - + + { + my ($src,$symb,$anchor,$stack); + if ($args->{'sort'}) { + my $it = $navmap->getIterator(undef, undef, undef, 1); + while ( my $res=$it->next()) { + if (ref($res) && + $res->symb() eq $curRes->symb()) { last; } + } + $stack=$it->getStack(); + } else { + $stack=$it->getStack(); + } + ($src,$symb,$anchor)=getLinkForResource($stack); + if (defined($anchor)) { $anchor='#'.$anchor; } + my $srcHasQuestion = $src =~ /\?/; + $args->{"resourceLink"} = $src. + ($srcHasQuestion?'&':'?') . + 'symb=' . &escape($symb).$anchor; + } # Now, we've decided what parts to show. Loop through them and # show them. foreach my $part (@parts) { @@ -1859,22 +1648,6 @@ END # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; - 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/(\#.*)$//) { - $anchor=$1; - } - my $srcHasQuestion = $src =~ /\?/; - $args->{"resourceLink"} = $src. - ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($curRes->symb()). - $anchor; # Now, display each column. foreach my $col (@$cols) { @@ -1941,8 +1714,6 @@ if (location.href.indexOf('#curloc')==-1 $r->rflush(); } - if ($mustCloseNavMap) { $navmap->untieHashes(); } - return $result; } @@ -1954,8 +1725,8 @@ sub add_linkitem { sub show_linkitems { my ($linkitems)=@_; - my @linkorder = ("launchnav","closenav","firsthomework","everything", - "uncompleted","changefolder","clearbubbles"); + my @linkorder = ("blank","launchnav","closenav","firsthomework", + "everything","uncompleted","changefolder","clearbubbles"); my $result .= (< @@ -1970,7 +1741,7 @@ ENDBLOCK $result.='}
- '."\n"; foreach my $link (@linkorder) { if (defined($linkitems->{$link})) { if ($linkitems->{$link}{'text'} ne '') { @@ -1981,7 +1752,8 @@ ENDBLOCK } $result .= '  - '."\n"; + '."\n"; + return $result; } @@ -2004,15 +1776,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 @@ -2042,10 +1815,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 @@ -2058,6 +1827,8 @@ See iterator documentation below. use strict; use GDBM_File; +use Apache::lonnet; +use LONCAPA; sub new { # magic invocation to create a class instance @@ -2077,7 +1848,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; @@ -2103,56 +1874,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 $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 $userprefix=$uname.'_'.$udom.'_'; - - 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; @@ -2163,51 +1914,63 @@ 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); + &Apache::lonenc::check_decrypt(\$symb); + if (($fromcid ne '') && ($fromcid ne $cid)) { + next; + } + 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; @@ -2221,14 +1984,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 { @@ -2248,7 +2032,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"); @@ -2258,37 +2042,24 @@ sub courseMapDefined { sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, - shift, undef, shift); + shift, undef, shift, + shift, shift); 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 { 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 { @@ -2297,29 +2068,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 = &escape($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 @@ -2345,7 +2224,7 @@ the given map. This is one of the proper # The strategy here is to cache the resource objects, and only construct them # as we use them. The real point is to prevent reading any more from the tied -# hash then we have to, which should hopefully alleviate speed problems. +# hash than we have to, which should hopefully alleviate speed problems. sub getById { my $self = shift; @@ -2395,7 +2274,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); } @@ -2411,7 +2290,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); } @@ -2419,16 +2298,28 @@ sub finishResource { # the actual lookup; parmval caches the results. sub parmval { my $self = shift; - my ($what,$symb)=@_; + my ($what,$symb,$recurse)=@_; my $hashkey = $what."|||".$symb; if (defined($self->{PARM_CACHE}->{$hashkey})) { - return $self->{PARM_CACHE}->{$hashkey}; + if (ref($self->{PARM_CACHE}->{$hashkey}) eq 'ARRAY') { + if (defined($self->{PARM_CACHE}->{$hashkey}->[0])) { + if (wantarray) { + return @{$self->{PARM_CACHE}->{$hashkey}}; + } else { + return $self->{PARM_CACHE}->{$hashkey}->[0]; + } + } + } else { + return $self->{PARM_CACHE}->{$hashkey}; + } } - - my $result = $self->parmval_real($what, $symb); + my $result = $self->parmval_real($what, $symb, $recurse); $self->{PARM_CACHE}->{$hashkey} = $result; - return $result; + if (wantarray) { + return @{$result}; + } + return $result->[0]; } sub parmval_real { @@ -2438,16 +2329,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 ''; } + 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\_//; @@ -2455,7 +2352,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; @@ -2471,39 +2372,50 @@ sub parmval_real { # ---------------------------------------------------------- first, check user if ($uname and defined($useropt)) { - if (defined($$useropt{$courselevelr})) { return $$useropt{$courselevelr}; } - if (defined($$useropt{$courselevelm})) { return $$useropt{$courselevelm}; } - if (defined($$useropt{$courselevel})) { return $$useropt{$courselevel}; } + if (defined($$useropt{$courselevelr})) { return [$$useropt{$courselevelr},'resource']; } + if (defined($$useropt{$courselevelm})) { return [$$useropt{$courselevelm},'map']; } + if (defined($$useropt{$courselevel})) { return [$$useropt{$courselevel},'course']; } } # ------------------------------------------------------- second, check course + if ($cgroup ne '' and defined($courseopt)) { + if (defined($$courseopt{$grplevelr})) { return [$$courseopt{$grplevelr},'resource']; } + if (defined($$courseopt{$grplevelm})) { return [$$courseopt{$grplevelm},'map']; } + if (defined($$courseopt{$grplevel})) { return [$$courseopt{$grplevel},'course']; } + } + if ($csec and defined($courseopt)) { - if (defined($$courseopt{$seclevelr})) { return $$courseopt{$seclevelr}; } - if (defined($$courseopt{$seclevelm})) { return $$courseopt{$seclevelm}; } - if (defined($$courseopt{$seclevel})) { return $$courseopt{$seclevel}; } + if (defined($$courseopt{$seclevelr})) { return [$$courseopt{$seclevelr},'resource']; } + if (defined($$courseopt{$seclevelm})) { return [$$courseopt{$seclevelm},'map']; } + if (defined($$courseopt{$seclevel})) { return [$$courseopt{$seclevel},'course']; } } if (defined($courseopt)) { - if (defined($$courseopt{$courselevelr})) { return $$courseopt{$courselevelr}; } - if (defined($$courseopt{$courselevelm})) { return $$courseopt{$courselevelm}; } - if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } + if (defined($$courseopt{$courselevelr})) { return [$$courseopt{$courselevelr},'resource']; } } # ----------------------------------------------------- third, check map parms my $thisparm=$$parmhash{$symbparm}; - if (defined($thisparm)) { return $thisparm; } + if (defined($thisparm)) { return [$thisparm,'map']; } # ----------------------------------------------------- fourth , check default my $meta_rwhat=$rwhat; $meta_rwhat=~s/\./_/g; my $default=&Apache::lonnet::metadata($fn,$meta_rwhat); - if (defined($default)) { return $default} + if (defined($default)) { return [$default,'resource']} $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat); - if (defined($default)) { return $default} - -# --------------------------------------------------- fifth , cascade up parts + if (defined($default)) { return [$default,'resource']} +# --------------------------------------------------- fifth, check more course + if (defined($courseopt)) { + if (defined($$courseopt{$courselevelm})) { return [$$courseopt{$courselevelm},'map']; } + if (defined($$courseopt{$courselevel})) { + my $ret = [$$courseopt{$courselevel},'course']; + return $ret; + } + } +# --------------------------------------------------- sixth , cascade up parts my ($space,@qualifier)=split(/\./,$rwhat); my $qualifier=join('.',@qualifier); @@ -2512,27 +2424,30 @@ sub parmval_real { my $id=pop(@parts); my $part=join('_',@parts); if ($part eq '') { $part='0'; } - my $partgeneral=$self->parmval($part.".$qualifier",$symb,1); - if (defined($partgeneral)) { return $partgeneral; } + my @partgeneral=$self->parmval($part.".$qualifier",$symb,1); + if (defined($partgeneral[0])) { return \@partgeneral; } } - if ($recurse) { return undef; } - my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$what); - if (defined($pack_def)) { return $pack_def; } - return ''; + if ($recurse) { return []; } + my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$rwhat); + if (defined($pack_def)) { return [$pack_def,'resource']; } + return ['']; } =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 which case 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): +=item * B(map, filterFunc, recursive, bailout, showall): The map is a specification of a map to retreive the resources from, either as a url or as an object. The filterFunc is a reference to a @@ -2540,13 +2455,14 @@ function that takes a resource object as true if the resource should be included, or false if it should not be. If recursive is true, the map will be recursively examined, otherwise it will not be. If bailout is true, the function will return -as soon as it finds a resource, if false it will finish. By default, -the map is the top-level map of the course, filterFunc is a function -that always returns 1, recursive is true, bailout is false. The -resources will be returned in a list containing the resource objects -for the corresponding resources, with B in -the list; regardless of branching, recursion, etc., it will be a flat -list. +as soon as it finds a resource, if false it will finish. If showall is +true it will not hide maps that contain nothing but one other map. By +default, the map is the top-level map of the course, filterFunc is a +function that always returns 1, recursive is true, bailout is false, +showall is false. The resources will be returned in a list containing +the resource objects for the corresponding resources, with B in the list; regardless of branching, +recursion, etc., it will be a flat list. Thus, this is suitable for cases where you don't want the structure, just a list of all resources. It is also suitable for finding out how @@ -2555,31 +2471,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 +Convenience 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 { @@ -2593,7 +2528,7 @@ sub retrieveResources { if (!defined($recursive)) { $recursive = 1; } my $bailout = shift; if (!defined($bailout)) { $bailout = 0; } - + my $showall = shift; # Create the necessary iterator. if (!ref($map)) { # assume it's a url of a map. $map = $self->getResourceByUrl($map); @@ -2612,10 +2547,14 @@ sub retrieveResources { # Get an iterator. my $it = $self->getIterator($map->map_start(), $map->map_finish(), - undef, $recursive); + undef, $recursive, $showall); my @resources = (); + if (&$filterFunc($map)) { + push(@resources, $map); + } + # Run down the iterator and collect the resources. my $curRes; @@ -2625,7 +2564,7 @@ sub retrieveResources { next; } - push @resources, $curRes; + push(@resources, $curRes); if ($bailout) { return @resources; @@ -2642,13 +2581,22 @@ 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 Scalar::Util qw(weaken); +use Apache::lonnet; =pod @@ -2763,7 +2711,7 @@ be the tokens described above. Also note there is some old code floating around that trys to track the depth of the iterator to see when it's done; do not copy that -code. It is difficult to get right and harder to understand then +code. It is difficult to get right and harder to understand than this. They should be migrated to this new style. =cut @@ -2789,7 +2737,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 @@ -2909,7 +2857,8 @@ sub new { Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, - $self->{CONDITION}, 0); + $self->{CONDITION}, + $self->{FORCE_TOP}); } @@ -2946,6 +2895,10 @@ sub next { $self->{HAVE_RETURNED_0} = 1; return $self->{NAV_MAP}->getById('0.0'); } + if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0_BEGIN_MAP}) { + $self->{HAVE_RETURNED_0_BEGIN_MAP} = 1; + return $self->BEGIN_MAP(); + } if ($self->{RECURSIVE_ITERATOR_FLAG}) { # grab the next from the recursive iterator @@ -3047,7 +3000,7 @@ sub next { } # Is this the end of a branch? If so, all of the resources examined above - # led to lower levels then the one we are currently at, so we push a END_BRANCH + # led to lower levels than the one we are currently at, so we push a END_BRANCH # marker onto the stack so we don't forget. # Example: For the usual A(BC)(DE)F case, when the iterator goes down the # BC branch and gets to C, it will see F as the only next resource, but it's @@ -3072,7 +3025,9 @@ sub next { $self->{RECURSIVE_ITERATOR} = Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, - $self->{ALREADY_SEEN}, $self->{CONDITION}); + $self->{ALREADY_SEEN}, + $self->{CONDITION}, + $self->{FORCE_TOP}); } # If this is a blank resource, don't actually return it. @@ -3125,6 +3080,8 @@ sub populateStack { 1; package Apache::lonnavmaps::DFSiterator; +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 @@ -3153,7 +3110,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(); @@ -3252,9 +3209,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; } } @@ -3307,7 +3264,7 @@ sub populateStack { 1; package Apache::lonnavmaps::resource; - +use Scalar::Util qw(weaken); use Apache::lonnet; =pod @@ -3379,7 +3336,7 @@ X X All resources also have Bs, which uniquely identify a resource in a course. Many internal LON-CAPA functions expect a symb. A symb carries along with it the URL of the resource, and the map it appears -in. Symbs are much larger then resource IDs. +in. Symbs are much larger than resource IDs. =cut @@ -3389,7 +3346,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. @@ -3455,8 +3412,13 @@ false. =item * B: -Returns true for a map if the randompick feature is being used on the -map. (?) +Returns the number of randomly picked items for a map if the randompick +feature is being used on the map. + +=item * B: + +Returns true for a map if the randomorder feature is being used on the +map. =item * B: @@ -3477,6 +3439,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 @@ -3485,13 +3448,37 @@ 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'}; + my $randompick = $self->parmval('randompick'); + return $randompick; +} +sub randomorder { + my $self = shift; + my $randomorder = $self->parmval('randomorder'); + return ($randomorder =~ /^yes$/i); +} +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 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+)/; @@ -3500,16 +3487,41 @@ 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 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 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(); @@ -3520,6 +3532,7 @@ sub compTitle { } return $title; } + =pod B @@ -3560,6 +3573,17 @@ sub retrieveResources { return $self->{NAV_MAP}->retrieveResources(@_); } +sub is_exam { + my ($self,$part) = @_; + my $type = $self->parmval('type',$part); + if ($type eq 'exam') { + return 1; + } + if ($self->src() =~ /\.(exam)$/) { + return 1; + } + return 0; +} sub is_html { my $self=shift; my $src = $self->src(); @@ -3572,11 +3596,32 @@ 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) = @_; + my $type = $self->parmval('type',$part); + if ($type 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 is_raw_problem { + my $self=shift; + my $src = $self->src(); + if ($src =~ /\.(problem|exam|quiz|assess|survey|form|library|task)$/) { + return 1; + } + return 0; +} + sub contains_problem { my $self=shift; if ($self->is_page()) { @@ -3585,16 +3630,25 @@ sub contains_problem { } return 0; } +sub map_contains_problem { + my $self=shift; + if ($self->is_map()) { + my $has_problem= + $self->hasResource($self,sub { $_[0]->is_problem() },1); + return $has_problem; + } + return 0; +} 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'; } sub is_survey { my $self = shift(); my $part = shift(); - if ($self->parmval('type',$part) eq 'survey') { + my $type = $self->parmval('type',$part); + if ($type eq 'survey') { return 1; } if ($self->src() =~ /\.(survey)$/) { @@ -3602,6 +3656,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; @@ -3657,7 +3716,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; @@ -3670,7 +3729,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; @@ -3687,9 +3746,9 @@ sub map_type { # These functions will be responsible for returning the CORRECT # VALUE for the parameter, no matter what. So while they may look -# like direct calls to parmval, they can be more then that. +# like direct calls to parmval, they can be more than that. # So, for instance, the duedate function should use the "duedatetype" -# information, rather then the resource object user. +# information, rather than the resource object user. =pod @@ -3765,16 +3824,19 @@ Get the weight for the problem. sub acc { (my $self, my $part) = @_; - return $self->parmval("acc", $part); + my $acc = $self->parmval("acc", $part); + return $acc; } sub answerdate { (my $self, my $part) = @_; # Handle intervals - if ($self->parmval("answerdate.type", $part) eq 'date_interval') { - return $self->duedate($part) + - $self->parmval("answerdate", $part); + my $answerdatetype = $self->parmval("answerdate.type", $part); + my $answerdate = $self->parmval("answerdate", $part); + my $duedate = $self->parmval("duedate", $part); + if ($answerdatetype eq 'date_interval') { + $answerdate = $duedate + $answerdate; } - return $self->parmval("answerdate", $part); + return $answerdate; } sub awarded { my $self = shift; my $part = shift; @@ -3782,40 +3844,72 @@ 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 $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); } + my $date; + my @interval=$self->parmval("interval", $part); + my $due_date=$self->parmval("duedate", $part); + if ($interval[0] =~ /\d+/) { + my $first_access=&Apache::lonnet::get_first_access($interval[1], + $self->symb); + if (defined($first_access)) { + my $interval = $first_access+$interval[0]; + $date = (!$due_date || $interval < $due_date) ? $interval + : $due_date; + } else { + $date = $due_date; + } + } else { + $date = $due_date; + } + return $date; +} +sub handgrade { + (my $self, my $part) = @_; + my @response_ids = $self->responseIds($part); + if (@response_ids) { + foreach my $response_id (@response_ids) { + my $handgrade = $self->parmval("handgrade",$part.'_'.$response_id); + if (lc($handgrade) eq 'yes') { + return 'yes'; + } + } } - return $self->parmval("duedate", $part); + my $handgrade = $self->parmval("handgrade", $part); + return $handgrade; } sub maxtries { (my $self, my $part) = @_; - return $self->parmval("maxtries", $part); + my $maxtries = $self->parmval("maxtries", $part); + return $maxtries; } sub opendate { (my $self, my $part) = @_; - if ($self->parmval("opendate.type", $part) eq 'date_interval') { - return $self->duedate($part) - - $self->parmval("opendate", $part); + my $opendatetype = $self->parmval("opendate.type", $part); + my $opendate = $self->parmval("opendate", $part); + if ($opendatetype eq 'date_interval') { + my $duedate = $self->duedate($part); + $opendate = $duedate - $opendate; } - return $self->parmval("opendate"); + return $opendate; } sub problemstatus { (my $self, my $part) = @_; - return lc $self->parmval("problemstatus", $part); + my $problemstatus = $self->parmval("problemstatus", $part); + return lc($problemstatus); } sub sig { (my $self, my $part) = @_; - return $self->parmval("sig", $part); + my $sig = $self->parmval("sig", $part); + return $sig; } sub tol { (my $self, my $part) = @_; - return $self->parmval("tol", $part); + my $tol = $self->parmval("tol", $part); + return $tol; } -sub tries { +sub tries { my $self = shift; my $tries = $self->queryRestoreHash('tries', shift); if (!defined($tries)) { return '0';} @@ -3823,15 +3917,17 @@ sub tries { } sub type { (my $self, my $part) = @_; - return $self->parmval("type", $part); + my $type = $self->parmval("type", $part); + return $type; } 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'}); + my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $self->symb(), $env{'user.domain'}, + $env{'user.name'}, + $env{'request.course.sec'}); + return $weight; } sub part_display { my $self= shift(); my $partID = shift(); @@ -3882,6 +3978,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 @@ -3889,8 +4000,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. @@ -3902,18 +4013,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 @@ -3999,7 +4122,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)); } @@ -4074,8 +4197,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})) { @@ -4100,8 +4223,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 @@ -4110,8 +4233,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 = ''; @@ -4123,8 +4247,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); } } } @@ -4173,13 +4304,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 ... } @@ -4340,14 +4471,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; } @@ -4451,6 +4585,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; @@ -4466,17 +4601,36 @@ sub status { #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; } if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } - my $suppressFeedback = $self->problemstatus($part) eq 'no'; + my $suppressFeedback = 0; + if (($self->problemstatus($part) eq 'no') || + ($self->problemstatus($part) eq 'no_feedback_ever')) { + $suppressFeedback = 1; + } # 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) { @@ -4563,6 +4717,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, @@ -4683,6 +4838,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); @@ -4711,7 +4867,8 @@ sub browsePriv { return $self->{BROWSE_PRIV}; } - $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre', $self->src()); + $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre',$self->src(), + $self->symb()); } =pod
'. - &Apache::loncommon::help_open_menu('','Navigation Screen','Navigation_Screen','',undef,'RAT').' 
   '.$args->{'sort_html'}.'