--- loncom/interface/lonnavmaps.pm 2004/06/29 22:32:11 1.264 +++ loncom/interface/lonnavmaps.pm 2007/01/05 06:43:34 1.396 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.264 2004/06/29 22:32:11 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.396 2007/01/05 06:43:34 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -30,12 +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 used +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; } @@ -53,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 => '' @@ -77,189 +82,39 @@ 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 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; - - # 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']); - -# ----------------------------------------------------- Force menu registration - my $addentries=''; - if ($ENV{'form.register'}) { - $addentries=' onLoad="'.&Apache::lonmenu::loadevents(). - '" onUnload="'.&Apache::lonmenu::unloadevents().'"'; - $r->print(&Apache::lonmenu::registerurl(1)); - } - - # Header - $r->print(''. - &Apache::loncommon::bodytag('Navigate Course Contents','', - $addentries,'','',$ENV{'form.register'})); - $r->print(''. - &Apache::loncommon::help_open_topic('Navigation_Screen'). - &Apache::loncommon::help_open_bug('RAT')); - - $r->rflush(); - - # Check that it's defined - if (!($navmap->courseMapDefined())) { - $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"; - } - } - - my $jumpToFirstHomework = 0; - # Check to see if the student is jumping to next open, do-able problem - if ($ENV{QUERY_STRING} eq '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 { - $r->print("" . - &mt("Go To 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{QUERY_STRING} eq 'showOnlyHomework') { - $showOnlyHomework = 1; - $suppressEmptySequences = 1; - $filterFunc = sub { my $res = shift; - return $res->completable() || $res->is_map(); - }; - $r->print("

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

"); - $ENV{'form.filter'} = ''; - $ENV{'form.condition'} = 1; - $resource_no_folder_link = 1; - } else { - $r->print("" . - &mt("Show Only Uncompleted Homework")."    "); - } - - # renderer call - my $renderArgs = { 'cols' => [0,1,2,3], - 'url' => '/adm/navmaps', - 'navmap' => $navmap, - 'suppressNavmap' => 1, - 'suppressEmptySequences' => $suppressEmptySequences, - 'filterFunc' => $filterFunc, - 'resource_no_folder_link' => $resource_no_folder_link, - 'r' => $r}; - 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; +sub close { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + return(< +window.status='Accessing Nav Control'; +menu=window.open("/adm/rat/empty.html","loncapanav", + "height=600,width=400,scrollbars=1"); +window.status='Closing Nav Control'; +menu.close(); +window.status='Done.'; + +ENDCLOSE +} + +sub update { + if ($env{'environment.remotenavmap'} ne 'on') { return ''; } + if (!$env{'request.course.id'}) { return ''; } + if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } + return(< + +ENDUPDATE } # Convenience functions: Returns a string that adds or subtracts @@ -285,7 +140,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; @@ -293,14 +147,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); } } } @@ -309,11 +167,11 @@ 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(); + return ($res->link(),$res->shown_symb()); } # Convenience function: This separates the logic of how to create @@ -334,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) { @@ -369,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"; @@ -407,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'}; } @@ -422,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'); @@ -494,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 %G 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); } } @@ -713,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 @@ -729,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 @@ -759,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 @@ -792,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 @@ -861,6 +737,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}; @@ -869,28 +753,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 @@ -902,27 +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 = ""; @@ -930,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--; } @@ -945,6 +845,7 @@ sub render_resource { } # Decide what to display + $result .= "$newBranchText$linkopen$icon$linkclose"; my $curMarkerBegin = ''; @@ -953,27 +854,29 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '> '; + $curMarkerBegin = '>'; $curMarkerEnd = '<'; $params->{'displayedHereMarker'} = 1; } if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { - my $displaypart=&Apache::lonnet::EXT('resource.'.$part.'.display', - $resource->symb()); - unless ($displaypart) { $displaypart=$part; } - $partLabel = " (Part: $displaypart)"; - $link.='#'.&Apache::lonnet::escape($part); + my $displaypart=$resource->part_display($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()).')'; } - if (!$params->{'resource_nolink'} && !$resource->is_sequence()) { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + my $target; + if ($env{'environment.remotenavmap'} eq 'on') { + $target=' target="loncapaclient" '; + } + if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { + $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } else { $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; } @@ -986,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').''; } } @@ -1010,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').''; } } @@ -1026,7 +933,7 @@ sub render_communication_status { $discussionHTML = $feedbackHTML = $errorHTML = ''; } - return "$discussionHTML$feedbackHTML$errorHTML "; + return "$discussionHTML$feedbackHTML$errorHTML "; } sub render_quick_status { @@ -1036,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() && @@ -1045,7 +956,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"; } @@ -1057,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"; @@ -1176,18 +1089,23 @@ sub setDefault { return $val; } +sub cmp_title { + my ($atitle,$btitle) = (lc($_[0]->compTitle),lc($_[1]->compTitle)); + $atitle=~s/^\s*//; + $btitle=~s/^\s*//; + return $atitle cmp $btitle; +} + sub render { my $args = shift; &Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); my $result = ''; - # Configure the renderer. my $cols = $args->{'cols'}; if (!defined($cols)) { # no columns, no nav maps. return ''; } - my $mustCloseNavMap = 0; my $navmap; if (defined($args->{'navmap'})) { $navmap = $args->{'navmap'}; @@ -1208,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"; } } @@ -1228,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 = $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); @@ -1261,7 +1192,7 @@ sub render { # We only need to do this if we need to open the maps to show the # current position. This will change the counter so we can't count # for the jump marker with this loop. - while (($curRes = $mapIterator->next()) && !$found) { + while ($here && ($curRes = $mapIterator->next()) && !$found) { if (ref($curRes) && $curRes->symb() eq $here) { my $mapStack = $mapIterator->getStack(); @@ -1278,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 @@ -1337,23 +1267,24 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - + # Print key? if ($printKey) { $result .= ''; my $date=localtime; $result.=''; + my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($navmap->{LAST_CHECK}) { $result .= - ' '.&mt('New discussion since').' '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). ''; } else { $result .= ''; } @@ -1361,18 +1292,81 @@ sub render { } if ($printCloseAll && !$args->{'resource_no_folder_link'}) { + my ($link,$text); if ($condition) { - $result.="".&mt('Close All Folders').""; + $link='"navmaps?condition=0&filter=&'.$queryString. + '&here='.&escape($here).'"'; + $text='Close all folders'; } else { - $result.="".&mt('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', + 'location.href='.$link,$text); + } else { + $result.=''.&mt($text).''; + } + $result .= "\n"; + } + + # Check for any unread discussions in all resources. + if ($args->{'caller'} eq 'navmapsdisplay') { + &add_linkitem($args->{'linkitems'},'clearbubbles', + 'document.clearbubbles.submit()', + 'Mark all posts read'); + my $time=time; + $result .= (< + + +END + if ($args->{'sort'} eq 'discussion') { + my $totdisc = 0; + my $haveDisc = ''; + my @allres=$navmap->retrieveResources(); + foreach my $resource (@allres) { + if ($resource->hasDiscussion()) { + $haveDisc .= $resource->wrap_symb().':'; + $totdisc ++; + } + } + if ($totdisc > 0) { + $haveDisc =~ s/:$//; + $result .= (< + +END + } + } + $result.=''; + } + + if ($args->{'caller'} eq 'navmapsdisplay') { + $result .= '
Key:    '. - ' '.&mt('New message (click to open)').'

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

'. '

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

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

'. + $args->{'sort_html'}.'
'; + } elsif ($args->{'sort_html'}) { + $result.=$args->{'sort_html'}; + } + $result .= "
\n"; if ($r) { $r->print($result); $r->rflush(); @@ -1394,7 +1388,9 @@ sub render { $args->{'indentLevel'} = 0; $args->{'isNewBranch'} = 0; $args->{'condensed'} = 0; - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + my $location= + &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); + $args->{'indentString'} = setDefault($args->{'indentString'}, "  "); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1444,7 +1440,60 @@ sub render { $args->{'here'} = $here; $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0 - while ($curRes = $it->next($closeAllPages)) { + my @resources; + my $code='';# sub { !(shift->is_map();) }; + if ($args->{'sort'} eq 'title') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if ($res->is_map()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } elsif ($args->{'sort'} eq 'duedate') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->is_problem()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { + if ($a->duedate ne $b->duedate) { + return $a->duedate cmp $b->duedate; + } + my $value=&cmp_title($a,$b); + return $value; + } @resources; + } elsif ($args->{'sort'} eq 'discussion') { + my $oldFilterFunc = $filterFunc; + my $filterFunc= + sub { + my ($res)=@_; + if (!$res->hasDiscussion() && + !$res->getFeedback() && + !$res->getErrors()) { return 0;} + return &$oldFilterFunc($res); + }; + @resources=$navmap->retrieveResources(undef,$filterFunc); + @resources= sort { &cmp_title($a,$b) } @resources; + } else { + #unknow sort mechanism or default + undef($args->{'sort'}); + } + + + while (1) { + if ($args->{'sort'}) { + $curRes = shift(@resources); + } else { + $curRes = $it->next($closeAllPages); + } + if (!$curRes) { last; } + # Maintain indentation level. if ($curRes == $it->BEGIN_MAP() || $curRes == $it->BEGIN_BRANCH() ) { @@ -1494,10 +1543,6 @@ sub render { $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]); @@ -1547,7 +1592,26 @@ sub render { # 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) { @@ -1558,18 +1622,7 @@ sub render { # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; - my $stack = $it->getStack(); - my $src = getLinkForResource($stack); - my $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) { my $colHTML = ''; @@ -1635,8 +1688,46 @@ if (location.href.indexOf('#curloc')==-1 $r->rflush(); } - if ($mustCloseNavMap) { $navmap->untieHashes(); } + return $result; +} +sub add_linkitem { + my ($linkitems,$name,$cmd,$text)=@_; + $$linkitems{$name}{'cmd'}=$cmd; + $$linkitems{$name}{'text'}=&mt($text); +} + +sub show_linkitems { + my ($linkitems)=@_; + my @linkorder = ("blank","launchnav","closenav","firsthomework", + "everything","uncompleted","changefolder","clearbubbles"); + + my $result .= (< + +
+   +
'."\n"; + return $result; } @@ -1659,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 @@ -1697,10 +1789,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 @@ -1713,6 +1801,7 @@ See iterator documentation below. use strict; use GDBM_File; +use Apache::lonnet; sub new { # magic invocation to create a class instance @@ -1732,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; @@ -1758,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 $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; @@ -1818,56 +1887,64 @@ 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); - my $plain=&Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); - if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { - my ($what,$url)=($1,$2); - my %status= - &Apache::lonnet::get('email_status',[$msgid]); - if ($status{$msgid}=~/^error\:/) { - $status{$msgid}=''; - } - - if (($status{$msgid} eq 'new') || - (!$status{$msgid})) { - if ($what eq 'Error') { - $error{$url}.=','.$msgid; - } else { - $feedback{$url}.=','.$msgid; - } - } + foreach my $msgid (@keys) { + if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { + 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; + } + } + } } } + #symbs of resources that have feedbacks (will be urls pre-2.3) $self->{FEEDBACK} = \%feedback; - $self->{ERROR_MSG} = \%error; # what is this? JB + #or errors (will be urls pre 2.3) + $self->{ERROR_MSG} = \%error; $self->{DISCUSSION_TIME} = \%discussiontime; $self->{EMAIL_STATUS} = \%emailstatus; $self->{LAST_READ} = \%lastreadtime; @@ -1880,14 +1957,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 { @@ -1907,7 +2005,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"); @@ -1917,67 +2015,163 @@ 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|bulletin___\d+___adm/wrapper|) { - $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 { - return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_CHECK}; +# return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_CHECK}; # v.1.1 behavior + return $self->{DISCUSSION_TIME}->{$ressymb} > 0; # in 1.2 will display speech bubble icons for all items with posts until marked as read (even if read in v 1.1). } } +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 @@ -2023,9 +2217,14 @@ sub getById { sub getBySymb { my $self = shift; my $symb = shift; + my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb); my $map = $self->getResourceByUrl($mapUrl); - return $self->getById($map->map_pc() . '.' . $id); + my $returnvalue = undef; + if (ref($map)) { + $returnvalue = $self->getById($map->map_pc() .'.'.$id); + } + return $returnvalue; } sub getByMapPc { @@ -2048,7 +2247,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); } @@ -2064,7 +2263,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); } @@ -2091,16 +2290,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\_//; @@ -2108,7 +2313,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; @@ -2130,6 +2339,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}; } @@ -2138,8 +2353,6 @@ sub parmval_real { if (defined($courseopt)) { if (defined($$courseopt{$courselevelr})) { return $$courseopt{$courselevelr}; } - if (defined($$courseopt{$courselevelm})) { return $$courseopt{$courselevelm}; } - if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } } # ----------------------------------------------------- third, check map parms @@ -2156,7 +2369,13 @@ sub parmval_real { $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat); if (defined($default)) { return $default} -# --------------------------------------------------- fifth , cascade up parts +# --------------------------------------------------- fifth, check more course + if (defined($courseopt)) { + if (defined($$courseopt{$courselevelm})) { return $$courseopt{$courselevelm}; } + if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } + } + +# --------------------------------------------------- sixth , cascade up parts my ($space,@qualifier)=split(/\./,$rwhat); my $qualifier=join('.',@qualifier); @@ -2176,16 +2395,19 @@ 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): +=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 @@ -2193,13 +2415,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 @@ -2208,30 +2431,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 { @@ -2245,7 +2488,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); @@ -2264,7 +2507,7 @@ sub retrieveResources { # Get an iterator. my $it = $self->getIterator($map->map_start(), $map->map_finish(), - undef, $recursive); + undef, $recursive, $showall); my @resources = (); @@ -2294,13 +2537,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 @@ -2441,7 +2693,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 @@ -2561,7 +2813,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}); } @@ -2724,7 +2977,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. @@ -2777,6 +3032,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 @@ -2805,7 +3062,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(); @@ -2904,9 +3161,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; } } @@ -2959,7 +3216,7 @@ sub populateStack { 1; package Apache::lonnavmaps::resource; - +use Scalar::Util qw(weaken); use Apache::lonnet; =pod @@ -3041,7 +3298,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. @@ -3129,6 +3386,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 @@ -3137,13 +3395,31 @@ 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; + 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+)/; @@ -3152,16 +3428,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(); @@ -3212,6 +3513,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(); @@ -3224,10 +3535,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; @@ -3239,7 +3561,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'; } @@ -3254,6 +3575,17 @@ 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; + my $src = $self->src(); + return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc()); +} # Private method: Shells out to the parmval in the nav map, handler parts. sub parmval { @@ -3303,7 +3635,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; @@ -3316,7 +3648,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; @@ -3428,14 +3760,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) = @_; @@ -3475,10 +3821,19 @@ 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(); + if (! defined($partID)) { $partID = '0'; } + my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display', + $self->symb); + if (! defined($display) || $display eq '') { + $display = $partID; + } + return $display; } # Multiple things need this @@ -3519,6 +3874,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 @@ -3526,8 +3896,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. @@ -3539,18 +3909,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 @@ -3568,6 +3950,16 @@ Returns the number of parts of the probl for single part problems, returns 1. For multipart, it returns the number of parts in the problem, not including psuedo-part 0. +=item * B(): + +Returns the total number of responses in the problem a student can answer. + +=item * B(): + +Returns a hash whose keys are the response types. The values are the number +of times each response type is used. This is for the I problem, not +just a single part. + =item * B(): Returns true if the problem is multipart, false otherwise. Use this instead @@ -3614,6 +4006,26 @@ sub countParts { return scalar(@{$parts}); # + $delta; } +sub countResponses { + my $self = shift; + my $count; + foreach my $part (@{$self->parts()}) { + $count+= scalar($self->responseIds($part)); + } + return $count; +} + +sub responseTypes { + my $self = shift; + my %responses; + foreach my $part (@{$self->parts()}) { + foreach my $responsetype ($self->responseType($part)) { + $responses{$responsetype}++ if (defined($responsetype)); + } + } + return %responses; +} + sub multipart { my $self = shift; return $self->countParts() > 1; @@ -3681,8 +4093,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})) { @@ -3701,13 +4113,14 @@ sub extractParts { } + # These hashes probably do not need names that end with "Hash".... my %responseIdHash; my %responseTypeHash; # 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 @@ -3716,8 +4129,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 = ''; @@ -3729,24 +4143,41 @@ 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); } } } } my $resorder = &Apache::lonnet::metadata($self->src(),'responseorder'); + # + # Reorder the arrays in the %responseIdHash and %responseTypeHash if ($resorder) { my @resorder=split(/,/,$resorder); foreach my $part (keys(%responseIdHash)) { - my %resids = map { ($_,1) } @{ $responseIdHash{$part} }; + my $i=0; + my %resids = map { ($_,$i++) } @{ $responseIdHash{$part} }; my @neworder; foreach my $possibleid (@resorder) { if (exists($resids{$possibleid})) { - push(@neworder,$possibleid); + push(@neworder,$resids{$possibleid}); } } - $responseIdHash{$part}=\@neworder; + my @ids; + my @type; + foreach my $element (@neworder) { + push (@ids,$responseIdHash{$part}->[$element]); + push (@type,$responseTypeHash{$part}->[$element]); + } + $responseIdHash{$part}=\@ids; + $responseTypeHash{$part}=\@type; } } $self->{RESPONSE_IDS} = \%responseIdHash; @@ -3769,13 +4200,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 ... } @@ -3936,13 +4367,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_override') {return $self->CORRECT_BY_OVERRIDE; } + if ($status eq 'correct_by_scantron') {return $self->CORRECT;} + if ($status eq 'correct_by_override') { + return $self->CORRECT_BY_OVERRIDE; + } if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } if ($status eq 'excused') {return $self->EXCUSED; } @@ -4046,6 +4481,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; @@ -4064,14 +4500,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) { @@ -4092,7 +4543,7 @@ sub status { if ($dateStatus == PAST_DUE_ANSWER_LATER || $dateStatus == PAST_DUE_NO_ANSWER ) { - return $dateStatus; + return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus; } if ($dateStatus == ANSWER_OPEN) { @@ -4158,6 +4609,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, @@ -4278,6 +4730,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); @@ -4306,7 +4759,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