--- loncom/interface/lonnavmaps.pm 2003/05/12 18:22:38 1.185 +++ loncom/interface/lonnavmaps.pm 2003/05/16 14:17:08 1.191 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.185 2003/05/12 18:22:38 bowersj2 Exp $ +# $Id: lonnavmaps.pm,v 1.191 2003/05/16 14:17:08 bowersj2 Exp $ # # Copyright Michigan State University Board of Trustees # @@ -200,8 +200,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 +218,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 +246,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(); @@ -737,6 +767,14 @@ returns a true or false value. If true, false, it is simply skipped in the display. By default, all resources are shown. +=item * B: + +If you're using a filter function, and displaying sequences to orient +the user, then frequently some sequences will be empty. Setting this to +true will cause those sequences not to display, so as not to confuse the +user into thinking that if the sequence is there there should be things +under it. + =item * B: If true, will not display Navigate Content resources. Default to @@ -1033,6 +1071,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; @@ -1129,6 +1180,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(); @@ -1161,8 +1214,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) { @@ -1223,6 +1274,43 @@ sub render { $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; + # If we're suppressing empty sequences, look for them here. Use DFS for speed, + # since structure actually doesn't matter, except what map has what resources. + if ($args->{'suppressEmptySequences'}) { + my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap, + $it->{FIRST_RESOURCE}, + $it->{FINISH_RESOURCE}, + {}, undef, 1); + $depth = 0; + $dfsit->next(); + my $curRes = $dfsit->next(); + while ($depth > -1) { + if ($curRes == $dfsit->BEGIN_MAP()) { $depth++; } + if ($curRes == $dfsit->END_MAP()) { $depth--; } + + if (ref($curRes)) { + # Parallel pre-processing: Do sequences have non-filtered-out children? + if ($curRes->is_sequence()) { + $curRes->{DATA}->{HAS_VISIBLE_CHILDREN} = 0; + # Sequences themselves do not count as visible children, + # unless those sequences also have visible children. + # This means if a sequence appears, there's a "promise" + # that there's something under it if you open it, somewhere. + } else { + # Not a sequence: if it's filtered, ignore it, otherwise + # rise up the stack and mark the sequences as having children + if (&$filterFunc($curRes)) { + for my $sequence (@{$dfsit->getStack()}) { + $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1; + } + } + } + } + } continue { + $curRes = $dfsit->next(); + } + } + my $displayedJumpMarker = 0; # Set up iteration. $depth = 1; @@ -1258,19 +1346,25 @@ sub render { next; } - $args->{'counter'}++; - # If this has been filtered out, continue on if (!(&$filterFunc($curRes))) { $args->{'isNewBranch'} = 0; # Don't falsely remember this next; } + # If this is an empty sequence and we're filtering them, continue on + if ($curRes->is_sequence() && $args->{'suppressEmptySequences'} && + !$curRes->{DATA}->{HAS_VISIBLE_CHILDREN}) { + next; + } + # If we're suppressing navmaps and this is a navmap, continue on if ($suppressNavmap && $curRes->src() =~ /^\/adm\/navmaps/) { next; } + $args->{'counter'}++; + # Does it have multiple parts? $args->{'multipart'} = 0; $args->{'condensed'} = 0; @@ -2194,7 +2288,8 @@ sub new { $curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} - } + } + } continue { $curRes = $iterator->next(); } } @@ -2571,6 +2666,31 @@ sub next { return $self->{HERE}; } +# Identical to the full iterator methods of the same name. Hate to copy/paste +# but I also hate to "inherit" either iterator from the other. + +sub getStack { + my $self=shift; + + my @stack; + + $self->populateStack(\@stack); + + return \@stack; +} + +# Private method: Calls the iterators recursively to populate the stack. +sub populateStack { + my $self=shift; + my $stack = shift; + + push @$stack, $self->{HERE} if ($self->{HERE}); + + if ($self->{RECURSIVE_ITERATOR_FLAG}) { + $self->{RECURSIVE_ITERATOR}->populateStack($stack); + } +} + 1; package Apache::lonnavmaps::resource; @@ -2594,6 +2714,12 @@ You will probably never need to instanti Apache::lonnavmaps::navmap, and use the "start" method to obtain the starting resource. +Resource objects respect the parameter_hiddenparts, which suppresses +various parts according to the wishes of the map author. As of this +writing, there is no way to override this parameter, and suppressed +parts will never be returned, nor will their response types or ids be +stored. + =head2 Public Members resource objects have a hash called DATA ($resourceRef->{DATA}) that @@ -3094,9 +3220,18 @@ each part. Filtering part 0 if you want 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 +B may return an array with more parts in it then countParts might lead you to believe. +=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): + +Retreives the response ID for the given part, which may be an empty string. + =back =cut @@ -3114,28 +3249,38 @@ 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 responseType { + my $self = shift; + my $part = shift; + + $self->extractParts(); + return $self->{RESPONSE_TYPE}->{$part}; } -sub partType { +sub responseId { my $self = shift; my $part = shift; $self->extractParts(); - return $self->{PART_TYPE}->{$part}; + return $self->{RESPONSE_IDS}->{$part}; } # Private function: Extracts the parts information, both part names and -# part types, and saves it +# part types, and saves it. sub extractParts { my $self = shift; @@ -3174,6 +3319,46 @@ sub extractParts { my @sortedParts = sort keys %parts; $self->{PARTS} = \@sortedParts; + + my %responseIdHash; + my %responseTypeHash; + + + # Init the responseIdHash + foreach (@{$self->{PARTS}}) { + $responseIdHash{$_} = []; + } + + # Now, the unfortunate thing about this is that parts, part name, and + # response if are delimited by underscores, but both the part + # name and response id can themselves have underscores in them. + # 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_(.*)/) { + my $responseType = $1; + my $partStuff = $2; + my $partIdSoFar = ''; + my @partChunks = split /_/, $partStuff; + my $i = 0; + + for ($i = 0; $i < scalar(@partChunks); $i++) { + if ($partIdSoFar) { $partIdSoFar .= '_'; } + $partIdSoFar .= $partChunks[$i]; + if ($parts{$partIdSoFar}) { + my @otherChunks = @partChunks[$i+1..$#partChunks]; + my $responseId = join('_', @otherChunks); + push @{$responseIdHash{$partIdSoFar}}, $responseId; + $responseTypeHash{$partIdSoFar} = $responseType; + last; + } + } + } + } + + $self->{RESPONSE_IDS} = \%responseIdHash; + $self->{RESPONSE_TYPES} = \%responseTypeHash; } return; @@ -3392,7 +3577,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 @@ -3481,7 +3668,7 @@ sub status { if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } - my $suppressFeedback = $self->parmval("problemstatus", $part) eq 'No'; + my $suppressFeedback = lc($self->parmval("problemstatus", $part)) eq 'no'; # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || @@ -3535,6 +3722,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