--- loncom/interface/lonnavmaps.pm 2002/09/24 01:18:50 1.51 +++ loncom/interface/lonnavmaps.pm 2002/10/01 19:15:22 1.59 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.51 2002/09/24 01:18:50 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.59 2002/10/01 19:15:22 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -124,32 +124,32 @@ sub parmval { my $courselevelm=$usercourseprefix.'.'.$mapparm; # ---------------------------------------------------------- first, check user - if ($uname) { - if ($useropt{$courselevelr}) { return $useropt{$courselevelr}; } - if ($useropt{$courselevelm}) { return $useropt{$courselevelm}; } - if ($useropt{$courselevel}) { return $useropt{$courselevel}; } + if (defined($uname)) { + if (defined($useropt{$courselevelr})) { return $useropt{$courselevelr}; } + if (defined($useropt{$courselevelm})) { return $useropt{$courselevelm}; } + if (defined($useropt{$courselevel})) { return $useropt{$courselevel}; } } # ------------------------------------------------------- second, check course - if ($csec) { - if ($courseopt{$seclevelr}) { return $courseopt{$seclevelr}; } - if ($courseopt{$seclevelm}) { return $courseopt{$seclevelm}; } - if ($courseopt{$seclevel}) { return $courseopt{$seclevel}; } + if (defined($csec)) { + if (defined($courseopt{$seclevelr})) { return $courseopt{$seclevelr}; } + if (defined($courseopt{$seclevelm})) { return $courseopt{$seclevelm}; } + if (defined($courseopt{$seclevel})) { return $courseopt{$seclevel}; } } - if ($courseopt{$courselevelr}) { return $courseopt{$courselevelr}; } - if ($courseopt{$courselevelm}) { return $courseopt{$courselevelm}; } - if ($courseopt{$courselevel}) { return $courseopt{$courselevel}; } + 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 my $thisparm=$parmhash{$symbparm}; - if ($thisparm) { return $thisparm; } + if (defined($thisparm)) { return $thisparm; } # ----------------------------------------------------- fourth , check default my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); - if ($default) { return $default} + if (defined($default)) { return $default} # --------------------------------------------------- fifth , cascade up parts @@ -159,10 +159,10 @@ sub parmval { my ($part,$id)=split(/\_/,$space); if ($id) { my $partgeneral=&parmval($part.".$qualifier",$symb); - if ($partgeneral) { return $partgeneral; } + if (defined($partgeneral)) { return $partgeneral; } } else { my $resourcegeneral=&parmval("0.$qualifier",$symb); - if ($resourcegeneral) { return $resourcegeneral; } + if (defined($resourcegeneral)) { return $resourcegeneral; } } } return ''; @@ -827,21 +827,6 @@ sub new_handle { $ENV{"request.course.fn"}.".db", $ENV{"request.course.fn"}."_parms.db", 1); - # Grab a resource object so we have access to the constants; this - # is technically not proper, but should be harmless - my $res = $navmap->firstResource(); - - # Defines a status->color mapping, null string means don't color - my %colormap = - ( $res->NETWORK_FAILURE => '', - $res->CORRECT => '#BBFFBB', - $res->EXCUSED => '#BBBBFF', - $res->PAST_DUE => '#FFAA00', - $res->ANSWER_OPEN => '#FF00AA', - $res->OPEN_LATER => '', - $res->TRIES_LEFT => '#FFFF00', - $res->INCORRECT => '#FFAA00', - $res->OPEN => '#FFFF88' ); if (!defined($navmap)) { my $requrl = $r->uri; @@ -849,6 +834,53 @@ sub new_handle { return HTTP_NOT_ACCEPTABLE; } + # Check that it's defined + if (!($navmap->courseMapDefined())) { + $r->print('Coursemap undefined.' . + ''); + return OK; + } + + # Grab a resource object so we have access to the constants; this + # is technically not proper, but should be harmless + my $res = $navmap->firstResource(); + + # Defines a status->color mapping, null string means don't color + my %colormap = + ( $res->NETWORK_FAILURE => '', + $res->CORRECT => '', + $res->EXCUSED => '#BBBBFF', + $res->PAST_DUE_ANSWER_LATER => '', + $res->PAST_DUE_NO_ANSWER => '', + $res->ANSWER_OPEN => '#CCFFCC', + $res->OPEN_LATER => '', + $res->TRIES_LEFT => '', + $res->INCORRECT => '', + $res->OPEN => '', + $res->NOTHING_SET => '' ); + # 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 = "#FFCCCC"; + + my %statusIconMap = + ( $res->NETWORK_FAILURE => '', + $res->NOTHING_SET => '', + $res->CORRECT => 'navmap.correct.gif', + $res->EXCUSED => 'navmap.correct.gif', + $res->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', + $res->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', + $res->ANSWER_OPEN => 'navmap.wrong.gif', + $res->OPEN_LATER => '', + $res->TRIES_LEFT => 'navmap.open.gif', + $res->INCORRECT => 'navmap.wrong.gif', + $res->OPEN => 'navmap.open.gif' ); + + my %condenseStatuses = + ( $res->NETWORK_FAILURE => 1, + $res->NOTHING_SET => 1, + $res->CORRECT => 1, + $res->OPEN => 1 ); + my %filterHash; # Figure out what we're not displaying foreach (split(/\,/, $ENV{"form.filter"})) { @@ -858,46 +890,88 @@ sub new_handle { } # Begin the HTML table - # two columns: the title/icon on the left, the info on the right + # four cols: resource + indent, chat+feedback, icon, text string $r->print('' ."\n"); my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash); my $curRes = $mapIterator->next(); + undef $res; # so we don't accidentally use it later my $indentLevel = -1; - my $indentString = "        "; + my $indentString = ""; my $isNewBranch = 0; + my $now = time(); + my $in24Hours = $now + 24 * 60 * 60; while ($curRes != $mapIterator->END_NAV_MAP) { if ($curRes == $mapIterator->BEGIN_MAP() || - $curRes == $mapIterator->BEGIN_BRANCH()) { + $curRes == $mapIterator->BEGIN_BRANCH()) { $indentLevel++; } if ($curRes == $mapIterator->END_MAP() || - $curRes == $mapIterator->END_BRANCH()) { + $curRes == $mapIterator->END_BRANCH()) { $indentLevel--; } - - if ($curRes == $mapIterator->BEGIN_BRANCH()) { - $r->print("Begin branch
"); - } - if ($curRes == $mapIterator->END_BRANCH()) { - $r->print("End branch
"); + if ($curRes == $mapIterator->BEGIN_BRANCH()) { + $isNewBranch = 1; } if (ref($curRes) && $curRes->src()) { + # Step one: Decide which parts to show my @parts; if ($curRes->is_problem()) { + # Is it multipart? @parts = @{$curRes->parts()}; - if (scalar(@parts) > 1) { shift(@parts); } + + my $multipart = scalar(@parts) > 1; + + if ($multipart) { + # If it's multipart, see if part 0 is "open" + # if it is, display all parts, if it isn't, + # just display first + if (!$curRes->opendate("0")) { + @parts = ("0"); # just display the zero-th part + } else { + # Otherwise, only display part 0 if we want to + # attach feedback or email information to it + if (!$curRes->hasDiscussion() && !$curRes->getFeedback()) { + shift @parts; + } else { + # If there's discussion or feedback, that counts + # as a difference, so display the parts. + + # Now, we decide whether to condense the + # parts due to similarity + my $status = $curRes->status($parts[0]); + my $allSame = 1; + for (my $i = 1; $i < scalar(@parts); $i++) { + if ($curRes->status($parts[$i]) != $status){ + $allSame = 0; + } + } + + # $allSame is true if all the statuses were + # the same. Now, if they are all the same and + # match one of the statuses to condense, then + # only display part 0. + if ($allSame && defined($condenseStatuses{$status})){ + @parts = ("0"); + } + } + } + } + } else { @parts[0] = "0"; # this is to get past foreach loop below # you can consider a non-problem resource as a resource # with only one part without loss } + # Step two: Show the parts foreach my $part (@parts) { + + my $deltalevel = 0; # for inserting the branch icon # For each thing we're displaying... @@ -911,6 +985,15 @@ sub new_handle { '"'; my $title = $curRes->title(); my $partLabel = ""; + my $newBranchText = ""; + + # If this is a new branch, label it so + # (temporary, this should be an icon w/ alt text) + if ($isNewBranch) { + $newBranchText = ""; + $isNewBranch = 0; + $deltalevel = 1; + } # links to open and close the folders my $linkopen = ""; @@ -927,7 +1010,7 @@ sub new_handle { my $nowOpen = !defined($filterHash{$mapId}); $icon = $nowOpen ? "folder_opened.gif" : "folder_closed.gif"; - $linkopen = "". - ""); + + my $discussionHTML = ""; my $feedbackHTML = ""; + + # SECOND ROW: Is there text or feedback? + if ($curRes->hasDiscussion()) { + $discussionHTML = $linkopen . + '' . + $linkclose; + } + + if ($curRes->getFeedback()) { + my $feedback = $curRes->getFeedback(); + foreach (split(/\,/, $feedback)) { + if ($_) { + $feedbackHTML .= ' ' + . ''; + } + } + } + + $r->print(""); + + # THIRD ROW: Problem status icon + if ($curRes->is_problem()) { + my $icon = $statusIconMap{$curRes->status()}; + if ($icon) { + $r->print("\n"); + } else { + $r->print("\n"); + } + } else { # not problem, no icon + $r->print("\n"); + } + + # FOURTH ROW: Text description + $r->print("\n"); } } $curRes = $mapIterator->next(); @@ -971,6 +1094,8 @@ sub new_handle { $r->print("
\n"); # print indentation - for (my $i = 0; $i < $indentLevel; $i++) { + for (my $i = 0; $i < $indentLevel - $deltalevel; $i++) { $r->print($indentString); } - $r->print(" ${linkopen}print(" ${newBranchText}${linkopen}\"\"${linkclose}\n"); if ($curRes->is_problem() && $part != "0") { $partLabel = " (Part $part)"; } - $r->print(" $title$partLabel\n"); + $r->print(" $title$partLabel$discussionHTML$feedbackHTML$linkopen$linkclose\n"); if ($curRes->kind() eq "res" and $curRes->is_problem() ) { - $r->print (" Due: " . localtime($curRes->duedate())); + $r->print (getDescription($curRes, $part)); } + + $r->print("
"); + $navmap->untieHashes(); + return OK; } @@ -1020,6 +1145,69 @@ sub getLinkForResource { return $res->src(); } +# Convenience function: This seperates the logic of how to create +# the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", +# etc.) into a seperate function. It takes a resource object as the +# first parameter, and the part number of the resource as the second. +# It's basically a big switch statement on the status of the resource. + +sub getDescription { + my $res = shift; + my $part = shift; + my $status = $res->getDateStatus(); + + if ($status == $res->NETWORK_FAILURE) { return ""; } + if ($status == $res->NOTHING_SET) { + return "Not currently assigned."; + } + if ($status == $res->OPEN_LATER) { + return "Opens: " . timeToHumanString($res->opendate($part)); + } + if ($status == $res->OPEN) { + if ($res->duedate()) { + return "Due: $status " . timeToHumanString($res->duedate($part)); + } else { + return "Open, no due date"; + } + } + if ($status == $res->PAST_DUE_ANSWER_LATER) { + return "Answer opens: " . timeToHumanString($res->answerdate($part)); + } + if ($status == $res->PAST_DUE_NO_ANSWER) { + return "Was Due: " . timeToHumanString($res->duedate($part)); + } + if ($status == $res->ANSWER_OPEN) { + return "Answer available"; + } + if ($status == $res->EXCUSED) { + return "Excused by instructor"; + } + if ($status == $res->TRIES_LEFT) { + my $tries = $res->tries(); + my $maxtries = $res->maxtries(); + my $triesString = "($tries of $maxtries tries used)"; + if ($res->duedate()) { + return "Due: $status " . timeToHumanString($res->duedate($part)) . + " $triesString"; + } else { + return "No due date $triesString"; + } + } +} + +# I want to change this into something more human-friendly. For +# now, this is a simple call to localtime. The final function +# probably belongs in loncommon. +sub timeToHumanString { + my ($time) = @_; + # zero, '0' and blank are bad times + if ($time) { + return localtime($time); + } else { + return 'Never'; + } +} + 1; package Apache::lonnavmaps::navmap; @@ -1044,7 +1232,7 @@ You must obtain resource objects through =over 4 -=item * B(filename, parmHashFile, genCourseAndUserOptions): Binds a new navmap object to the compiled course representation and parmHashFile. genCourseAndUserOptions is a flag saying whether the course options and user options hash should be generated. This is for when you are using the parameters of the resources that require them; see documentation in resource object documentation. Returns the navmap object if this is successful, or B if not. You must check for undef; errors will occur when you try to use the other methods otherwise. +=item * B(filename, parmHashFile, genCourseAndUserOptions, genMailDiscussStatus): Binds a new navmap object to the compiled course representation and parmHashFile. genCourseAndUserOptions is a flag saying whether the course options and user options hash should be generated. This is for when you are using the parameters of the resources that require them; see documentation in resource object documentation. genMailDiscussStatus causes the nav map to retreive information about the email and discussion status of resources. Returns the navmap object if this is successful, or B if not. You must check for undef; errors will occur when you try to use the other methods otherwise. =item * B(first, finish, filter, condition): See iterator documentation below. @@ -1062,6 +1250,7 @@ sub new { $self->{NAV_HASH_FILE} = shift; $self->{PARM_HASH_FILE} = shift; $self->{GENERATE_COURSE_USER_OPT} = shift; + $self->{GENERATE_EMAIL_DISCUSS_STATUS} = shift; # Resource cache stores navmapresource's as we reference them. We generate # them on-demand so we don't pay for creating resources unless we use them. @@ -1139,16 +1328,73 @@ sub new { $useropt{$userprefix.&Apache::lonnet::unescape($name)}= &Apache::lonnet::unescape($value); } - $self->{COURSE_OPT} = \%courseopt; - $self->{USER_OPT} = \%useropt; + $self->{COURSE_OPT} = \%courseopt; + $self->{USER_OPT} = \%useropt; } } + if ($self->{GENERATE_EMAIL_DISCUSS_STATUS}) { + my $cid=$ENV{'request.course.id'}; + my ($cdom,$cnum)=split(/\_/,$cid); + + my %emailstatus = &Apache::lonnet::dump('email_status'); + my $logoutTime = $emailstatus{'logout'}; + my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; + $self->{LAST_CHECK} = ($courseLeaveTime > $logoutTime ? + $courseLeaveTime : $logoutTime); + my %discussiontime = &Apache::lonnet::dump('discussiontimes', + $cdom, $cnum); + my %feedback=(); + my %error=(); + my $keys = &Apache::lonnet::reply('keys:'. + $ENV{'user.domain'}.':'. + $ENV{'user.name'}.':nohist_email', + $ENV{'user.home'}); + + 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; + } + } + } + } + + $self->{FEEDBACK} = \%feedback; + $self->{ERROR_MSG} = \%error; # what is this? JB + $self->{DISCUSSION_TIME} = \%discussiontime; + $self->{EMAIL_STATUS} = \%emailstatus; + + } + bless($self); - + return $self; } +# 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 $firstres = $self->{NAV_HASH}->{"map_start_$uri"}; + my $lastres = $self->{NAV_HASH}->{"map_finish_$uri"}; + return $firstres && $lastres; +} + sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, @@ -1170,6 +1416,29 @@ sub DESTROY { $self->untieHashes(); } +# Private function: 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; + if (!defined($self->{DISCUSSION_TIME})) { return 0; } + + return $self->{DISCUSSION_TIME}->{$symb} > + $self->{LAST_CHECK}; +} + +# Private function: 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; + + if (!defined($self->{FEEDBACK})) { return ""; } + + return $self->{FEEDBACK}->{$symb}; +} + =pod =item * B(id): Based on the ID of the resource (1.1, 3.2, etc.), get a resource object for that resource. This method, or other methods that use it (as in the resource object) is the only proper way to obtain a resource object. @@ -1264,33 +1533,33 @@ sub parmval { # ---------------------------------------------------------- first, check user if ($uname and defined($useropt)) { - if ($$useropt{$courselevelr}) { return $$useropt{$courselevelr}; } - if ($$useropt{$courselevelm}) { return $$useropt{$courselevelm}; } - if ($$useropt{$courselevel}) { return $$useropt{$courselevel}; } + if (defined($$useropt{$courselevelr})) { return $$useropt{$courselevelr}; } + if (defined($$useropt{$courselevelm})) { return $$useropt{$courselevelm}; } + if (defined($$useropt{$courselevel})) { return $$useropt{$courselevel}; } } # ------------------------------------------------------- second, check course if ($csec and defined($courseopt)) { - if ($$courseopt{$seclevelr}) { return $$courseopt{$seclevelr}; } - if ($$courseopt{$seclevelm}) { return $$courseopt{$seclevelm}; } - if ($$courseopt{$seclevel}) { return $$courseopt{$seclevel}; } + if (defined($$courseopt{$seclevelr})) { return $$courseopt{$seclevelr}; } + if (defined($$courseopt{$seclevelm})) { return $$courseopt{$seclevelm}; } + if (defined($$courseopt{$seclevel})) { return $$courseopt{$seclevel}; } } if (defined($courseopt)) { - if ($$courseopt{$courselevelr}) { return $$courseopt{$courselevelr}; } - if ($$courseopt{$courselevelm}) { return $$courseopt{$courselevelm}; } - if ($$courseopt{$courselevel}) { return $$courseopt{$courselevel}; } + 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 my $thisparm=$$parmhash{$symbparm}; - if ($thisparm) { return $thisparm; } + if (defined($thisparm)) { return $thisparm; } # ----------------------------------------------------- fourth , check default my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); - if ($default) { return $default} + if (defined($default)) { return $default} # --------------------------------------------------- fifth , cascade up parts @@ -1300,10 +1569,10 @@ sub parmval { my ($part,$id)=split(/\_/,$space); if ($id) { my $partgeneral=$self->parmval($part.".$qualifier",$symb); - if ($partgeneral) { return $partgeneral; } + if (defined($partgeneral)) { return $partgeneral; } } else { my $resourcegeneral=$self->parmval("0.$qualifier",$symb); - if ($resourcegeneral) { return $resourcegeneral; } + if (defined($resourcegeneral)) { return $resourcegeneral; } } } return ''; @@ -1460,7 +1729,7 @@ sub next { if (scalar(@{$self->{BRANCH_STACK}}) == 0) { if ($self->{BRANCH_DEPTH} > 0) { $self->{FORCE_NEXT} = $self->END_MAP(); - $self->{BRANCH_DEPTH}--; + $self->{BRANCH_DEPTH}--; return $self->END_BRANCH(); } else { return $self->END_MAP(); @@ -1479,13 +1748,32 @@ sub next { # to start a new one. (We know because we already checked to see # if the stack was empty.) if ( scalar (@{$self->{BRANCH_STACK}}) < $self->{BRANCH_STACK_SIZE}) { - $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); - $self->{BRANCH_DEPTH}++; - return $self->BEGIN_BRANCH(); + $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); + $self->{BRANCH_DEPTH}++; + return $self->BEGIN_BRANCH(); } + # Remember the size for comparision next time. + $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); + + # If the next resource we mean to return is going to need + # a lower branch level, terminate branches until we get + # there. + # Get the next resource in the branch $self->{HERE} = pop @{$self->{BRANCH_STACK}}; + + # Are we at the right depth? If not, close a branch and return + # the current resource onto the branch stack + if (defined($self->{HERE}->{DATA}->{ITERATOR_DEPTH}) + && $self->{HERE}->{DATA}->{ITERATOR_DEPTH} < + $self->{BRANCH_DEPTH} ) { + $self->{BRANCH_DEPTH}--; + # return it so we can pick it up eventually + push @{$self->{BRANCH_STACK}}, $self->{HERE}; + return $self->END_BRANCH(); + } + # We always return it after this point and never before # (proof: look at just the return statements), so we # remember that we've seen this. @@ -1493,23 +1781,23 @@ sub next { # Are we at the utter end? If so, return the END_NAV_MAP marker. if ($self->{HERE} == $self->{NAV_MAP}->finishResource() ) { - $self->{FORCE_NEXT} = $self->END_NAV_MAP; + $self->{FORCE_NEXT} = $self->END_NAV_MAP; return $self->{HERE}; } - # Remember the size for comparision next time. - $self->{BRANCH_STACK_SIZE} = scalar(@{$self->{BRANCH_STACK}}); - # Get the next possible resources my $nextUnfiltered = $self->{HERE}->getNext(); my $next = []; # filter the next possibilities to remove things we've - # already seen + # already seen. Also, remember what branch depth they should + # be displayed at, since there's no other reliable way to tell. foreach (@$nextUnfiltered) { - if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { - push @$next, $_; - } + if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { + push @$next, $_; + $_->{DATA}->{ITERATOR_DEPTH} = + $self->{BRANCH_DEPTH} + 1; + } } # Handle branch cases: @@ -1518,24 +1806,24 @@ sub next { # 2+ things next: have some branches my $nextCount = scalar(@$next); if ($nextCount == 0) { - # Return this and on the next run, close the branch up if we're - # in a branch - if ($self->{BRANCH_DEPTH} > 0 ) { - $self->{FORCE_NEXT} = $self->END_BRANCH(); - $self->{BRANCH_DEPTH}--; - } + # Return this and on the next run, close the branch up if we're + # in a branch + if ($self->{BRANCH_DEPTH} > 0 ) { + $self->{FORCE_NEXT} = $self->END_BRANCH(); + $self->{BRANCH_DEPTH}--; + } return $self->{HERE}; } while (@$next) { # copy the next possibilities over to the branch stack # in the right order - push @{$self->{BRANCH_STACK}}, shift @$next; + push @{$self->{BRANCH_STACK}}, shift @$next; } if ($nextCount >= 2) { - $self->{FORCE_NEXT} = $self->BEGIN_BRANCH(); - $self->{BRANCH_DEPTH}++; + $self->{FORCE_NEXT} = $self->BEGIN_BRANCH(); + $self->{BRANCH_DEPTH}++; return $self->{HERE}; } @@ -1552,8 +1840,8 @@ sub next { $self->{RECURSIVE_ITERATOR} = Apache::lonnavmaps::iterator->new ($self->{NAV_MAP}, $firstResource, $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}); - # prime the new iterator with the first resource - #push @{$self->{RECURSIVE_ITERATOR}->{BRANCH_STACK}}, $firstResource; + # prime the new iterator with the first resource + #push @{$self->{RECURSIVE_ITERATOR}->{BRANCH_STACK}}, $firstResource; } return $self->{HERE}; @@ -1883,6 +2171,30 @@ Misc. functions for the resource. =over 4 +=item * B: Returns a false value if there has been discussion since the user last logged in, true if there has. Always returns false if the discussion data was not extracted when the nav map was constructed. + +=item * B: Gets the feedback for the resource and returns the raw feedback string for the resource, or the null string if there is no feedback or the email data was not extracted when the nav map was constructed. Usually used like this: + + for (split(/\,/, $res->getFeedback())) { + my $link = &Apache::lonnet::escape($_); + ... + +and use the link as appropriate. + +=cut + +sub hasDiscussion { + my $self = shift; + return $self->{NAV_MAP}->hasDiscussion($self->symb()); +} + +sub getFeedback { + my $self = shift; + return $self->{NAV_MAP}->getFeedback($self->symb()); +} + +=pod + =item * B(): Returns a list reference containing sorted strings corresponding to each part of the problem. To count the number of parts, use the list in a scalar context, and subtract one if greater then two. (One part problems have a part 0. Multi-parts have a part 0, plus a part for each part. You may or may not wish to filter out part 0.) =item * B(): Returns the number of parts of the problem a student can answer. Thus, for single part problems, returns 1. For multipart, it returns the number of parts in the problem, not including psuedo-part 0. @@ -1950,7 +2262,7 @@ Idiomatic usage of these two methods wou =over 4 -=item * B($part): ($part defaults to 0). A convenience function that returns a symbolic constant telling you about the date status of the part, with the following possible results: +=item * B($part): ($part defaults to 0). A convenience function that returns a symbolic constant telling you about the date status of the part. You should still check to see if there is a due date at all, if you care about that. The possible return values are: =back @@ -1962,7 +2274,10 @@ B =item * B: Open and not yet due. -=item * B: The due date has passed, but the answer date has not yet arrived. + +=item * B: The due date has passed, but the answer date has not yet arrived. + +=item * B: The due date has passed and there is no answer opening date set. =item * B: The answer date is here. @@ -1973,28 +2288,45 @@ B =cut # Apparently the compiler optimizes these into constants automatically -sub OPEN_LATER { return 0; } -sub OPEN { return 1; } -sub PAST_DUE { return 2; } -sub ANSWER_OPEN { return 3; } -sub NETWORK_FAILURE { return 100; } +sub OPEN_LATER { return 0; } +sub OPEN { return 1; } +sub PAST_DUE_NO_ANSWER { return 2; } +sub PAST_DUE_ANSWER_LATER { return 3; } +sub ANSWER_OPEN { return 4; } +sub NOTHING_SET { return 5; } +sub NETWORK_FAILURE { return 100; } + +# getDateStatus gets the date status for a given problem part. +# Because answer date, due date, and open date are fully independent +# (i.e., it is perfectly possible to *only* have an answer date), +# we have to completely cover the 3x3 maxtrix of (answer, due, open) x +# (past, future, none given). This function handles this with a decision +# tree. Read the comments to follow the decision tree. sub getDateStatus { my $self = shift; my $part = shift; $part = "0" if (!defined($part)); + + # Always return network failure if there was one. return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE}); my $now = time(); - my $o = $now - $self->opendate($part); - my $d = $now - $self->duedate($part); - my $a = $now - $self->answerdate($part); - - if ($o < 0) {return $self->OPEN_LATER}; - if ($d < 0) {return $self->OPEN}; - if ($a < 0) {return $self->PAST_DUE}; - return $self->ANSWER_OPEN; + my $open = $self->opendate($part); + my $due = $self->duedate($part); + my $answer = $self->answerdate($part); + + if (!$open && !$due && !$answer) { + # no data on the problem at all + # should this be the same as "open later"? think multipart. + return $self->NOTHING_SET; + } + if (!$open || $now < $open) {return $self->OPEN_LATER} + if (!$due || $now < $due) {return $self->OPEN} + if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER} + if ($answer) { return $self->ANSWER_OPEN; } + return PAST_DUE_NO_ANSWER; } =pod @@ -2029,12 +2361,12 @@ B =cut -sub NOT_ATTEMPTED { return 0; } -sub INCORRECT { return 1; } -sub INCORRECT_BY_OVERRIDE { return 2; } -sub CORRECT { return 3; } -sub CORRECT_BY_OVERRIDE { return 4; } -sub EXCUSED { return 5; } +sub NOT_ATTEMPTED { return 10; } +sub INCORRECT { return 11; } +sub INCORRECT_BY_OVERRIDE { return 12; } +sub CORRECT { return 13; } +sub CORRECT_BY_OVERRIDE { return 14; } +sub EXCUSED { return 15; } sub getCompletionStatus { my $self = shift; @@ -2066,11 +2398,15 @@ Along with directly returning the date o =item * NETWORK_FAILURE: The network has failed and the information is not available. +=item * NOTHING_SET: No dates have been set for this problem (part) at all. (Because only certain parts of a multi-part problem may be assigned, this can not be collapsed into "open later", as we don't know a given part will EVER be opened. For single part, this is the same as "OPEN_LATER".) + =item * CORRECT: For any reason at all, the part is considered correct. =item * EXCUSED: For any reason at all, the problem is excused. -=item * PAST_DUE: The problem is past due, and not considered correct. +=item * PAST_DUE_NO_ANSWER: The problem is past due, not considered correct, and no answer date is set. + +=item * PAST_DUE_ANSWER_LATER: The problem is past due, not considered correct, and an answer date in the future is set. =item * ANSWER_OPEN: The problem is past due, not correct, and the answer is now available. @@ -2097,56 +2433,55 @@ sub status { # What we have is a two-dimensional matrix with 4 entries on one # dimension and 5 entries on the other, which we want to colorize, - # plus network failure. + # plus network failure and "no date data at all". - # Don't colorize on network failure. - if ($completionStatus == NETWORK_FAILURE()) { return $self->NETWORK_FAILURE(); } + if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } # There are a few whole rows we can dispose of: - # If the problem is CORRECT, color it green no matter what - if ($completionStatus == CORRECT() || - $completionStatus == CORRECT_BY_OVERRIDE() ) { - return $self->CORRECT(); # Return a nice green. + if ($completionStatus == CORRECT || + $completionStatus == CORRECT_BY_OVERRIDE ) { + return CORRECT(); + } + + # If it's EXCUSED, then return that no matter what + if ($completionStatus == EXCUSED) { + return EXCUSED; } - # If it's EXCUSED, then return something no matter what - if ($completionStatus == EXCUSED()) { - return $self->EXCUSED(); # return a nice blue + if ($dateStatus == NOTHING_SET) { + return NOTHING_SET; } # Now we're down to a 3 (incorrect, incorrect_override, not_attempted) # by 4 matrix (date status). - # If it's Past Due and we didn't bail earlier because it's correct, - # color it orange. (Red is sort inappropriate; too drastic a color - # for something the student can't fix. - if ($dateStatus == PAST_DUE()) { - return $self->PAST_DUE(); # return orange + if ($dateStatus == PAST_DUE_ANSWER_LATER || + $dateStatus == PAST_DUE_NO_ANSWER ) { + return $dateStatus; } - if ($dateStatus == ANSWER_OPEN()) { - return $self->ANSWER_OPEN(); + if ($dateStatus == ANSWER_OPEN) { + return ANSWER_OPEN; } # Now: (incorrect, incorrect_override, not_attempted) x # (open_later), (open) - # If it's open later, then don't colorize - if ($dateStatus == OPEN_LATER()) { - return $self->OPEN_LATER(); + if ($dateStatus == OPEN_LATER) { + return OPEN_LATER; } # If it's WRONG... - if ($completionStatus == INCORRECT() || $completionStatus == INCORRECT_BY_OVERRIDE()) { + if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { # and there are TRIES LEFT: if ($self->tries() < $self->maxtries()) { - return $self->TRIES_LEFT(); # return red: The student can fix this + return TRIES_LEFT; } - return $self->INCORRECT(); # otherwise, return orange; student can't fix this + return INCORRECT; # otherwise, return orange; student can't fix this } # Otherwise, it's untried and open - return $self->OPEN(); # Light yellow + return OPEN; } =pod