--- loncom/interface/lonnavmaps.pm 2003/05/14 20:16:56 1.190 +++ loncom/interface/lonnavmaps.pm 2003/06/12 13:20:27 1.199 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.190 2003/05/14 20:16:56 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.199 2003/06/12 13:20:27 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -47,6 +47,7 @@ use Apache::Constants qw(:common :http); use Apache::loncommon(); use Apache::lonmenu(); use POSIX qw (floor strftime); +use Data::Dumper; # for debugging, not always used # symbolic constants sub SYMB { return 1; } @@ -200,8 +201,10 @@ sub real_handler { } } + 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 $depth = 1; @@ -216,9 +219,7 @@ sub real_handler { if (ref($curRes) && $curRes->is_problem()) { my $status = $curRes->status(); - if (($status == $curRes->OPEN || - $status == $curRes->TRIES_LEFT()) && - $curRes->getCompletionStatus() != $curRes->ATTEMPTED()) { + if ($curRes->completable()) { $problemRes = $curRes; $foundDoableProblem = 1; @@ -246,15 +247,45 @@ sub real_handler { "Go To My First Homework Problem
"); } - # renderer call - my $render = render({ 'cols' => [0,1,2,3], - 'url' => '/adm/navmaps', - 'navmap' => $navmap, - 'suppressNavmap' => 1, - 'r' => $r}); + my $suppressEmptySequences = 0; + my $filterFunc = undef; + # 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_sequence(); + }; + $r->print("

Uncompleted Homework

"); + $ENV{'form.filter'} = ''; + $ENV{'form.condition'} = 1; + } else { + $r->print("" . + "Show Only Uncompleted Homework
"); + } + # renderer call + my $renderArgs = { 'cols' => [0,1,2,3], + 'url' => '/adm/navmaps', + 'navmap' => $navmap, + 'suppressNavmap' => 1, + 'suppressEmptySequences' => $suppressEmptySequences, + 'filterFunc' => $filterFunc, + '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("

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(); @@ -808,7 +839,7 @@ sub render_resource { my $icon = ""; if ($resource->is_problem()) { - if ($part eq "" || $params->{'condensed'}) { + if ($part eq '0' || $params->{'condensed'}) { $icon = ''; } else { $icon = $params->{'indentString'}; @@ -878,7 +909,7 @@ sub render_resource { $params->{'displayedHereMarker'} = 1; } - if ($resource->is_problem() && $part ne "" && + if ($resource->is_problem() && $part ne '0' && !$params->{'condensed'}) { $partLabel = " (Part $part)"; $title = ""; @@ -935,6 +966,10 @@ sub render_communication_status { } } + if ($params->{'multipart'} && $part != '0') { + $discussionHTML = $feedbackHTML = $errorHTML = ''; + } + return "$discussionHTML$feedbackHTML$errorHTML "; } @@ -970,7 +1005,7 @@ sub render_long_status { $params->{'multipart'} && $part eq "0"; my $color; - if ($resource->is_problem()) { + if ($resource->is_problem() && ($resource->countParts() <= 1 || $part ne '') ) { $color = $colormap{$resource->status}; if (dueInLessThen24Hours($resource, $part) || @@ -990,8 +1025,6 @@ sub render_long_status { $result .= '(randomly select ' . $resource->randompick() .')'; } - $result .= " \n"; - return $result; } @@ -1041,6 +1074,19 @@ sub render { } } + # Filter: Remember filter function and add our own filter: Refuse + # to show hidden resources unless the user can see them. + my $userCanSeeHidden = advancedUser(); + my $filterFunc = setDefault($args->{'filterFunc'}, + sub {return 1;}); + if (!$userCanSeeHidden) { + # Without renaming the filterfunc, the server seems to go into + # an infinite loop + my $oldFilterFunc = $filterFunc; + $filterFunc = sub { my $res = shift; return !$res->randomout() && + &$oldFilterFunc($res);}; + } + my $condition = 0; if ($ENV{'form.condition'}) { $condition = 1; @@ -1137,6 +1183,8 @@ sub render { } # (re-)Locate the jump point, if any + # Note this does not take filtering or hidden into account... need + # to be fixed? my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0); my $depth = 1; $mapIterator->next(); @@ -1169,8 +1217,6 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - my $filterFunc = setDefault($args->{'filterFunc'}, - sub {return 1;}); # Print key? if ($printKey) { @@ -1303,8 +1349,6 @@ sub render { next; } - $args->{'counter'}++; - # If this has been filtered out, continue on if (!(&$filterFunc($curRes))) { $args->{'isNewBranch'} = 0; # Don't falsely remember this @@ -1322,6 +1366,8 @@ sub render { next; } + $args->{'counter'}++; + # Does it have multiple parts? $args->{'multipart'} = 0; $args->{'condensed'} = 0; @@ -1330,7 +1376,7 @@ sub render { # Decide what parts to show. if ($curRes->is_problem() && $showParts) { @parts = @{$curRes->parts()}; - $args->{'multipart'} = scalar(@parts) > 1; + $args->{'multipart'} = $curRes->multipart(); if ($condenseParts) { # do the condensation if (!$curRes->opendate("0")) { @@ -1339,13 +1385,13 @@ sub render { } if (!$args->{'condensed'}) { # Decide whether to condense based on similarity - my $status = $curRes->status($parts[1]); - my $due = $curRes->duedate($parts[1]); - my $open = $curRes->opendate($parts[1]); + my $status = $curRes->status($parts[0]); + my $due = $curRes->duedate($parts[0]); + my $open = $curRes->opendate($parts[0]); my $statusAllSame = 1; my $dueAllSame = 1; my $openAllSame = 1; - for (my $i = 2; $i < scalar(@parts); $i++) { + for (my $i = 1; $i < scalar(@parts); $i++) { if ($curRes->status($parts[$i]) != $status){ $statusAllSame = 0; } @@ -1366,25 +1412,30 @@ sub render { if (($statusAllSame && defined($condenseStatuses{$status})) || ($dueAllSame && $status == $curRes->OPEN && $statusAllSame)|| ($openAllSame && $status == $curRes->OPEN_LATER && $statusAllSame) ){ - @parts = (); + @parts = ($parts[0]); $args->{'condensed'} = 1; } - } + # Multipart problem with one part: always "condense" (happens + # to match the desirable behavior) + if ($curRes->countParts() == 1) { + @parts = ($parts[0]); + $args->{'condensed'} = 1; + } } } # If the multipart problem was condensed, "forget" it was multipart if (scalar(@parts) == 1) { $args->{'multipart'} = 0; + } else { + # Add part 0 so we display it correctly. + unshift @parts, '0'; } # Now, we've decided what parts to show. Loop through them and # show them. - foreach my $part ('', @parts) { - if ($part eq '0') { - next; - } + foreach my $part (@parts) { $rownum ++; my $backgroundColor = $backgroundColors[$rownum % scalar(@backgroundColors)]; @@ -1618,7 +1669,7 @@ sub init { my %emailstatus = &Apache::lonnet::dump('email_status'); my $logoutTime = $emailstatus{'logout'}; my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; - $self->{LAST_CHECK} = ($courseLeaveTime < $logoutTime ? + $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? $courseLeaveTime : $logoutTime); my %discussiontime = &Apache::lonnet::dump('discussiontimes', $cdom, $cnum); @@ -1735,6 +1786,16 @@ object for that resource. This method, o (as in the resource object) is the only proper way to obtain a resource object. +=item * B(symb): + +Based on the symb of the resource, get a resource object for that +resource. This is one of the proper ways to get a resource object. + +=item * B(map_pc): + +Based on the map_pc of the resource, get a resource object for +the given map. This is one of the proper ways to get a resource object. + =cut # The strategy here is to cache the resource objects, and only construct them @@ -1765,6 +1826,14 @@ sub getBySymb { return $self->getById($map->map_pc() . '.' . $id); } +sub getByMapPc { + my $self = shift; + my $map_pc = shift; + my $map_id = $self->{NAV_HASH}->{'map_id_' . $map_pc}; + $map_id = $self->{NAV_HASH}->{'ids_' . $map_id}; + return $self->getById($map_id); +} + =pod =item * B(): @@ -3167,27 +3236,30 @@ sub getErrors { =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 than two. (One part -problems have a part 0. Multi-parts have a part 0, plus a part for -each part. Filtering part 0 if you want it is up to you.) +each part of the problem. Single part problems have only a part '0'. +Multipart problems do not return their part '0', since they typically +do not really matter. =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. Thus, -B may return an array with fewer parts in it then countParts -might lead you to believe. +number of parts in the problem, not including psuedo-part 0. + +=item * B(): + +Returns true if the problem is multipart, false otherwise. Use this instead +of countParts if all you want is multipart/not multipart. =item * B($part): Returns the response type of the part, without the word "response" on the end. Example return values: 'string', 'essay', 'numeric', etc. -=item * B($part): +=item * B($part): -Retreives the response ID for the given part, which may be an empty string. +Retreives the response IDs for the given part as an array reference containing +strings naming the response IDs. This may be empty. =back @@ -3196,7 +3268,7 @@ Retreives the response ID for the given sub parts { my $self = shift; - if ($self->ext) { return ['0']; } + if ($self->ext) { return []; } $self->extractParts(); return $self->{PARTS}; @@ -3206,16 +3278,23 @@ sub countParts { my $self = shift; my $parts = $self->parts(); - my $delta = 0; - for my $part (@$parts) { - if ($part eq '0') { $delta--; } - } + + # If I left this here, then it's not necessary. + #my $delta = 0; + #for my $part (@$parts) { + # if ($part eq '0') { $delta--; } + #} if ($self->{RESOURCE_ERROR}) { return 0; } - return scalar(@{$parts}) + $delta; + return scalar(@{$parts}); # + $delta; +} + +sub multipart { + my $self = shift; + return $self->countParts() > 1; } sub responseType { @@ -3226,7 +3305,7 @@ sub responseType { return $self->{RESPONSE_TYPE}->{$part}; } -sub responseId { +sub responseIds { my $self = shift; my $part = shift; @@ -3532,7 +3611,9 @@ B Along with directly returning the date or completion status, the resource object includes a convenience function B() that will combine the two status tidbits into one composite status that can -represent the status of the resource as a whole. The precise logic is +represent the status of the resource as a whole. This method represents +the concept of the thing we want to display to the user on the nav maps +screen, which is a combination of completion and open status. The precise logic is documented in the comments of the status method. The following results may be returned, all available as methods on the resource object ($res->NETWORK_FAILURE): In addition to the return values that match @@ -3675,6 +3756,48 @@ sub status { } =pod + +B + +The completable method represents the concept of I. If the student can do the problem, which means +that it is open, there are tries left, and if the problem is manually graded +or the grade is suppressed via problemstatus, the student has not tried it +yet, then the method returns 1. Otherwise, it returns 0, to indicate that +either the student has tried it and there is no feedback, or that for +some reason it is no longer completable (not open yet, successfully completed, +out of tries, etc.). As an example, this is used as the filter for the +"Uncompleted Homework" option for the nav maps. + +If this does not quite meet your needs, do not fiddle with it (unless you are +fixing it to better match the student's conception of "completable" because +it's broken somehow)... make a new method. + +=cut + +sub completable { + my $self = shift; + if (!$self->is_problem()) { return 0; } + my $partCount = $self->countParts(); + + foreach my $part (@{$self->parts()}) { + if ($part eq '0' && $partCount != 1) { next; } + my $status = $self->status($part); + # "If any of the parts are open, or have tries left (implies open), + # and it is not "attempted" (manually graded problem), it is + # not "complete" + if (!(($status == OPEN() || $status == TRIES_LEFT()) + && $self->getCompletionStatus($part) != ATTEMPTED() + && $status != ANSWER_SUBMITTED())) { + return 0; + } + } + + # If all the parts were complete, so was this problem. + return 1; +} + +=pod =head2 Resource/Nav Map Navigation