--- loncom/interface/lonnavmaps.pm 2005/04/15 22:03:23 1.326 +++ loncom/interface/lonnavmaps.pm 2006/06/05 17:55:59 1.387 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.326 2005/04/15 22:03:23 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.387 2006/06/05 17:55:59 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -40,6 +40,8 @@ use Apache::lonnet; use POSIX qw (floor strftime); use Data::Dumper; # for debugging, not always use Time::HiRes qw( gettimeofday tv_interval ); +use lib '/home/httpd/lib/perl/'; +use LONCAPA; # symbolic constants sub SYMB { return 1; } @@ -57,6 +59,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 => '' @@ -81,21 +84,28 @@ my %colormap = $resObj->OPEN => '', $resObj->NOTHING_SET => '', $resObj->ATTEMPTED => '', - $resObj->ANSWER_SUBMITTED => '' + $resObj->ANSWER_SUBMITTED => '', + $resObj->PARTIALLY_CORRECT => '#006600' ); # And a special case in the nav map; what to do when the assignment # is not yet done and due in less then 24 hours my $hurryUpColor = "#FF0000"; sub launch_win { - my ($mode,$script,$toplinkitems)=@_; + my ($mode,$script,$toplinkitems,$firsttime)=@_; my $result; if ($script ne 'no') { $result.=' - - - ENDSUBM - return; + $r->print(&Apache::loncommon::start_page(undef,$js, + {'only_body' => 1, + 'bgcolor' => '#FFFFFF', + 'add_entries' => + {'onload' => + "submitthis()"}}). + &Apache::loncommon::end_page()); + + return OK; } - if ($ENV{QUERY_STRING} eq 'launchExternal') { + if ($ENV{QUERY_STRING} =~ /^launchExternal/) { &Apache::lonnet::put('environment',{'remotenavmap' => 'on'}); &Apache::lonnet::appenv('environment.remotenavmap' => 'on'); + my $menu=&Apache::lonmenu::reopenmenu(); + my $navstatus=&Apache::lonmenu::get_nav_status(); + if ($menu) { + $r->print(< + swmenu=$menu + swmenu.clearTimeout(swmenu.menucltim); + $navstatus + +MENU + } + } + if ($ENV{QUERY_STRING} eq 'turningOffExternal') { + $env{'environment.remotenavmap'}='off'; } # Create the nav map @@ -216,49 +242,41 @@ ENDSUBM $env{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized"; return HTTP_NOT_ACCEPTABLE; } - my $html=&Apache::lonxml::xmlbegin(); - $r->print("$html\n"); - $r->print("".&mt('Navigate Course Contents').""); + $r->send_http_header; + # ------------------------------------------------------------ Get query string &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register','sort','showOnlyHomework','postsymb']); # ----------------------------------------------------- Force menu registration - my $addentries=''; - my $more_unload; my $body_only=''; + my $js; if ($env{'environment.remotenavmap'} eq 'on') { - $r->print(''); -# FIXME need to be smarter to only catch window close events -# $more_unload="collapse()" + $js=''; $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(''); + my $course_type = &Apache::loncommon::course_type(); + $r->print(&Apache::loncommon::start_page('Navigate '.$course_type. + ' Contents', + $js, + {'only_body' => $body_only, + 'force_register' => + $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.' . - ''); + $r->print(''.&mt('Coursemap undefined.'). + '' . + &Apache::loncommon::end_page()); return OK; } @@ -288,7 +306,7 @@ ENDSUBM if ($ENV{QUERY_STRING} eq 'launchExternal') { $r->print(' -
'); $r->print(' @@ -305,32 +323,38 @@ ENDSUBM "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; + my $minimumduedate; - while (($curRes = $iterator->next()) && !$foundDoableProblem) { + while ($curRes = $iterator->next()) { if (ref($curRes) && $curRes->is_problem()) { my $status = $curRes->status(); if ($curRes->completable()) { - $problemRes = $curRes; + my $thisduedate=$curRes->duedate(); + unless ($foundDoableProblem) { + $minimumduedate=$thisduedate; + } + $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 ($thisduedate<=$minimumduedate) { + # 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(); + $minimumduedate=$thisduedate; + } } } } @@ -342,7 +366,7 @@ ENDSUBM } else { &add_linkitem(\%toplinkitems,'firsthomework', 'location.href="navmaps?jumpToFirstHomework"', - "Show Me My First Homework Problem"); + "Show my first due problem"); } my $suppressEmptySequences = 0; @@ -359,8 +383,8 @@ ENDSUBM }; &add_linkitem(\%toplinkitems,'everything', 'location.href="navmaps?sort='.$env{'form.sort'}.'"', - "Show Everything"); - $r->print("

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

"); + "Show everything"); + $r->print("

".&mt("Uncompleted Problems")."

"); $env{'form.filter'} = ''; $env{'form.condition'} = 1; $resource_no_folder_link = 1; @@ -368,7 +392,7 @@ ENDSUBM &add_linkitem(\%toplinkitems,'uncompleted', 'location.href="navmaps?sort='.$env{'form.sort'}. '&showOnlyHomework=1"', - "Show Only Uncompleted Homework"); + "Show only uncompleted problems"); } my %selected=($env{'form.sort'} => 'selected=on'); @@ -411,7 +435,7 @@ ENDSUBM #my $td=&tv_interval($t0); #$r->print("
$td"); - $r->print(""); + $r->print(&Apache::loncommon::end_page()); $r->rflush(); return OK; @@ -450,14 +474,14 @@ sub getLinkForResource { my $anchor; if ($res->is_page()) { foreach (@$stack) { if (defined($_)) { $anchor = $_; } } - $anchor=&Apache::lonnet::escape($anchor->shown_symb()); + $anchor=&escape($anchor->shown_symb()); return ($res->link(),$res->shown_symb(),$anchor); } # in case folder was skipped over as "only sequence" my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); if ($map=~/\.page$/) { my $url=&Apache::lonnet::clutter($map); - $anchor=&Apache::lonnet::escape($src->shown_symb()); + $anchor=&escape($src->shown_symb()); return ($url,$res->shown_symb(),$anchor); } } @@ -492,22 +516,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) { @@ -527,7 +552,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"; @@ -580,8 +605,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'); @@ -652,30 +680,44 @@ sub timeToHumanString { return "$prefix$hourString$minuteString$tense"; } + # If there's a caller supplied format, use it. + + if($format ne '') { + my $timeStr = strftime($format, localtime($time)); + return $timeStr.&Apache::lonlocal::gettimezone($time); + } + # Less then 5 days away, display day of the week and # HH:MM + if ( $delta < $day * 5 ) { my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "next ") . - $timeStr; + return ($inPast ? "last " : "this ") . + $timeStr.&Apache::lonlocal::gettimezone($time); } + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } # Is it this year? if ( $time[5] == $now[5]) { # Return on Month Day, HH:MM meridian - my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); + my $timeStr = strftime("$conjunction %A, %b %e at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return $timeStr; + return $timeStr.&Apache::lonlocal::gettimezone($time); } # Not this year, so show the year - my $timeStr = strftime("on %A, %b %e %Y at %I:%M %P", localtime($time)); + my $timeStr = strftime("$conjunction %A, %b %e %Y at %I:%M %P", localtime($time)); $timeStr =~ s/12:00 am/00:00/; $timeStr =~ s/12:00 pm/noon/; - return $timeStr; + return $timeStr.&Apache::lonlocal::gettimezone($time); } } @@ -1025,7 +1067,6 @@ sub render_resource { # it will be quoted with ' in the href. my ($left,$right) = split(/\?/, $link); - $left =~ s/'/\\'/g; $link = $left.'?'.$right; my $src = $resource->src(); @@ -1039,28 +1080,28 @@ sub render_resource { my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = ""; + $newBranchText = "Branch"; } # links to open and close the folder - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon =''; + $icon =''.&mt('Problem').''; } else { $icon = $params->{'indentString'}; } } else { - $icon = ""; + $icon = "  "; } # Display the correct map icon to open or shut map @@ -1072,28 +1113,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 = "{'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'}) . + &escape($params->{'here'}) . '&jump=' . - &Apache::lonnet::escape($resource->symb()) . - "&folderManip=1'>"; + &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 = ""; @@ -1101,7 +1145,10 @@ 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 @@ -1125,7 +1172,7 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '> '; + $curMarkerBegin = '>'; $curMarkerEnd = '<'; $params->{'displayedHereMarker'} = 1; } @@ -1133,13 +1180,13 @@ sub render_resource { if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { my $displaypart=$resource->part_display($part); - $partLabel = " (Part: $displaypart)"; - if ($link!~/\#/) { $link.='#'.&Apache::lonnet::escape($part); } + $partLabel = " (".&mt('Part: [_1]', $displaypart).")"; + if ($link!~/\#/) { $link.='#'.&escape($part); } $title = ""; } if ($params->{'condensed'} && $resource->countParts() > 1) { - $nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; + $nonLinkedText .= ' ('.&mt('[_1] parts', $resource->countParts()).')'; } my $target; @@ -1147,7 +1194,7 @@ sub render_resource { $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"; } @@ -1160,7 +1207,11 @@ 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()) { @@ -1173,8 +1224,8 @@ sub render_communication_status { my $feedback = $resource->getFeedback(); foreach (split(/\,/, $feedback)) { if ($_) { - $feedbackHTML .= ' ' + $feedbackHTML .= ' ' . ''; } @@ -1188,8 +1239,8 @@ sub render_communication_status { last if ($errorcount>=10); # Only output 10 bombs maximum if ($_) { $errorcount++; - $errorHTML .= ' ' + $errorHTML .= ' ' . ''; } @@ -1210,7 +1261,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() && @@ -1369,7 +1424,6 @@ sub render { # no columns, no nav maps. return ''; } - my $mustCloseNavMap = 0; my $navmap; if (defined($args->{'navmap'})) { $navmap = $args->{'navmap'}; @@ -1418,22 +1472,27 @@ sub render { # 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'}) { + if ($env{'form.postsymb'} ne '') { $here = $jump = &Apache::lonnet::symbclean($env{'form.postsymb'}); - } elsif ($env{'form.postdata'}) { + } elsif ($env{'form.postdata'} ne '') { # couldn't find a symb, is there a URL? my $currenturl = $env{'form.postdata'}; #$currenturl=~s/^http\:\/\///; #$currenturl=~s/^[^\/]+//; $here = $jump = &Apache::lonnet::symbread($currenturl); - } else { + } + if ($here eq '') { my $last; if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db', &GDBM_READER(),0640)) { @@ -1482,7 +1541,6 @@ sub render { # 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 @@ -1555,12 +1613,12 @@ sub render { my ($link,$text); if ($condition) { $link='"navmaps?condition=0&filter=&'.$queryString. - '&here='.&Apache::lonnet::escape($here).'"'; - $text='Close All Folders'; + '&here='.&escape($here).'"'; + $text='Close all folders'; } else { $link='"navmaps?condition=1&filter=&'.$queryString. - '&here='.&Apache::lonnet::escape($here).'"'; - $text='Open All Folders'; + '&here='.&escape($here).'"'; + $text='Open all folders'; } if ($args->{'caller'} eq 'navmapsdisplay') { &add_linkitem($args->{'linkitems'},'changefolder', @@ -1650,7 +1708,7 @@ END $args->{'condensed'} = 0; my $location= &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); - $args->{'indentString'} = setDefault($args->{'indentString'}, ""); + $args->{'indentString'} = setDefault($args->{'indentString'}, "  "); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1803,10 +1861,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]); @@ -1874,7 +1928,7 @@ END my $srcHasQuestion = $src =~ /\?/; $args->{"resourceLink"} = $src. ($srcHasQuestion?'&':'?') . - 'symb=' . &Apache::lonnet::escape($symb).$anchor; + 'symb=' . &escape($symb).$anchor; } # Now, we've decided what parts to show. Loop through them and # show them. @@ -2014,15 +2068,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 @@ -2116,8 +2171,6 @@ sub generate_course_user_opt { my $cdom=$env{'course.'.$cid.'.domain'}; my $cnum=$env{'course.'.$cid.'.num'}; - my $useropt; my $courseopt; - # ------------------------------------------------- Get coursedata (if present) my $courseopt=&Apache::lonnet::get_courseresdata($cnum,$cdom); # Check for network failure @@ -2180,10 +2233,10 @@ sub generate_email_discuss_status { 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') { + &LONCAPA::unescape(&LONCAPA::unescape($msgid)); + if ($plain=~/ \[([^\]]+)\]\:/) { + my $url=$1; + if ($plain=~/\:Error \[/) { $error{$url}.=','.$msgid; } else { $feedback{$url}.=','.$msgid; @@ -2216,6 +2269,27 @@ sub get_user_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 { @@ -2271,10 +2345,66 @@ sub hasDiscussion { } } +sub last_post_time { + my $self = shift; + my $symb = shift; + my $ressymb = $self->wrap_symb($symb); + return $self->{DISCUSSION_TIME}->{$ressymb}; +} + +sub unread_discussion { + my $self = shift; + my $symb = 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 $unreadcount = 0; + my $hiddenflag = 0; + my $deletedflag = 0; + my ($hidden,$deleted); + + my %subjects; + + 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\./) + && $prevread < $self->{DISCUSSION_DATA}{$id.':'.$discsymb.':timestamp'}) { + $unreadcount++; + $subjects{$unreadcount}= + $id.': '.$self->{DISCUSSION_DATA}{$id.':'.$discsymb.':subject'}; + } + } + } + if (wantarray) { + return ($unreadcount,\%subjects); + } + return $unreadcount +} + sub wrap_symb { my $self = shift; my $symb = shift; - if ($symb =~ m-___(adm/\w+/\w+/)(\d+)(/bulletinboard)$-) { + if ($symb =~ m-___(adm/[^/]+/[^/]+/)(\d+)(/bulletinboard)$-) { unless ($symb =~ m|adm/wrapper/adm|) { $symb = 'bulletin___'.$2.'___adm/wrapper/'.$1.$2.$3; } @@ -2282,6 +2412,16 @@ sub wrap_symb { 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. @@ -2426,6 +2566,12 @@ sub parmval_real { 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'}; @@ -2433,7 +2579,7 @@ sub parmval_real { 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\_//; @@ -2443,6 +2589,10 @@ sub parmval_real { my $mapparm=$mapname.'___(all).'.$what; 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; my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm; @@ -2463,6 +2613,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}; } @@ -2513,14 +2669,17 @@ sub parmval_real { =pod -=item * B(url): +=item * B(url,multiple): -Retrieves a resource object by URL of the resource. If passed a -resource object, it will simply return it, so it is safe to use this -method in code like "$res = $navmap->getResourceByUrl($res)", if -you're not sure if $res is already an object, or just a URL. If the -resource appears multiple times in the course, only the first instance -will be returned. As a result, this is probably useful only for maps. +Retrieves a resource object by URL of the resource, unless the optional +multiple parameter is included in wahich caes an array of resource +objects is returned. If passed a resource object, it will simply return +it, so it is safe to use this method in code like +"$res = $navmap->getResourceByUrl($res)" +if you're not sure if $res is already an object, or just a URL. If the +resource appears multiple times in the course, only the first instance +will be returned (useful for maps), unless the multiple parameter has +been included, in which case all instances are returned in an array. =item * B(map, filterFunc, recursive, bailout, showall): @@ -2555,22 +2714,41 @@ Convience method for 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 { @@ -2638,10 +2816,16 @@ sub hasResource { return scalar($self->retrieveResources($map, $filterFunc, $recursive, 1, $showall)) > 0; } +sub usedVersion { + my $self = shift; + my $linkurl = shift; + return $self->navhash("version_$linkurl"); +} + 1; package Apache::lonnavmaps::iterator; -use WeakRef; +use Scalar::Util qw(weaken); use Apache::lonnet; =pod @@ -3122,7 +3306,7 @@ sub populateStack { 1; package Apache::lonnavmaps::DFSiterator; -use WeakRef; +use Scalar::Util qw(weaken); use Apache::lonnet; # Not documented in the perldoc: This is a simple iterator that just walks @@ -3306,7 +3490,7 @@ sub populateStack { 1; package Apache::lonnavmaps::resource; -use WeakRef; +use Scalar::Util qw(weaken); use Apache::lonnet; =pod @@ -3485,8 +3669,7 @@ sub kind { my $self=shift; return $self- sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } sub randompick { my $self = shift; - return $self->{NAV_MAP}->{PARM_HASH}->{$self->symb . - '.0.parameter_randompick'}; + return $self->parmval('randompick'); } sub link { my $self=shift; @@ -3502,6 +3685,15 @@ sub shown_symb { if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->symb());} return $self->symb(); } +sub id { + my $self=shift; + return $self->{ID}; +} +sub enclosing_map_src { + my $self=shift; + (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; + return $self->navHash('map_id_'.$first); +} sub symb { my $self=shift; (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; @@ -3533,7 +3725,18 @@ sub condition { my $condition=&Apache::lonnet::directcondval($condid); return $condition; } +sub condval { + my $self=shift; + my ($pathname,$filename) = + &Apache::lonnet::split_uri_for_cond($self->src()); + my $match=($env{'acc.res.'.$env{'request.course.id'}.'.'.$pathname}=~ + /\&\Q$filename\E\:([\d\|]+)\&/); + if ($match) { + return &Apache::lonnet::condval($1); + } + return 0; +} sub compTitle { my $self = shift; my $title = $self->title(); @@ -3584,6 +3787,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(); @@ -3596,10 +3809,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; @@ -3611,7 +3835,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'; } @@ -3626,6 +3849,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; @@ -3681,7 +3909,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; @@ -3694,7 +3922,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; @@ -3806,14 +4034,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 (defined($interval)) { 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) = @_; @@ -3906,6 +4148,19 @@ 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: + +returns in scalar context the count of the number of unread discussion +postings + +returns in list context both the count of postings and a hash ref +containing the subjects of all unread postings + =item * B: Gets the feedback for the resource and returns the raw feedback string @@ -3914,7 +4169,7 @@ email data was not extracted when the na used like this: for (split(/\,/, $res->getFeedback())) { - my $link = &Apache::lonnet::escape($_); + my $link = &escape($_); ... and use the link as appropriate. @@ -3926,6 +4181,16 @@ 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 unread_discussion { + my $self = shift; + return $self->{NAV_MAP}->unread_discussion($self->symb()); +} + sub getFeedback { my $self = shift; my $source = $self->src(); @@ -4023,7 +4288,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)); } @@ -4099,7 +4364,7 @@ sub extractParts { return; } foreach (split(/\,/,$metadata)) { - if ($_ =~ /^part_(.*)$/) { + if ($_ =~ /^(?:part|Task)_(.*)$/) { my $part = $1; # This floods the logs if it blows up if (defined($parts{$part})) { @@ -4134,8 +4399,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 = ''; @@ -4147,8 +4413,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); } } } @@ -4364,14 +4637,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; } @@ -4475,6 +4751,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; @@ -4493,14 +4770,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) { @@ -4587,6 +4879,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, @@ -4707,7 +5000,7 @@ sub getNext { my $to = $self->to(); foreach my $branch ( split(/,/, $to) ) { my $choice = $self->{NAV_MAP}->getById($branch); - if (!$choice->condition()) { next; } + #if (!$choice->condition()) { next; } my $next = $choice->goesto(); $next = $self->{NAV_MAP}->getById($next);