--- loncom/interface/lonnavmaps.pm 2023/01/20 22:45:51 1.509.2.14.2.6 +++ loncom/interface/lonnavmaps.pm 2025/01/16 21:26:55 1.509.2.14.2.10 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.509.2.14.2.6 2023/01/20 22:45:51 raeburn Exp $ +# $Id: lonnavmaps.pm,v 1.509.2.14.2.10 2025/01/16 21:26:55 raeburn Exp $ # # Copyright Michigan State University Board of Trustees @@ -534,6 +534,10 @@ my %colormap = $resObj->EXCUSED => '#3333FF', $resObj->PAST_DUE_ANSWER_LATER => '', $resObj->PAST_DUE_NO_ANSWER => '', + $resObj->PAST_DUE_ATMPT_ANS => '', + $resObj->PAST_DUE_ATMPT_NOANS => '', + $resObj->PAST_DUE_NO_ATMT_ANS => '', + $resObj->PAST_DUE_NO_ATMT_NOANS => '', $resObj->ANSWER_OPEN => '#006600', $resObj->OPEN_LATER => '', $resObj->TRIES_LEFT => '', @@ -679,10 +683,10 @@ sub getDescription { return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part).$slotinfo; } } - if ($status == $res->PAST_DUE_ANSWER_LATER) { + if (($status == $res->PAST_DUE_ANSWER_LATER) || ($status == $res->PAST_DUE_ATMPT_ANS) || ($status == $res->PAST_DUE_NO_ATMT_ANS)) { return &mt("Answer open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($answer,'start'),$res->symb(),'answerdate',$part)); } - if ($status == $res->PAST_DUE_NO_ANSWER) { + if (($status == $res->PAST_DUE_NO_ANSWER) || ($status == $res->PAST_DUE_ATMPT_NOANS) || ($status == $res->PAST_DUE_NO_ATMT_NOANS)) { if ($res->is_practice()) { return &mt("Closed [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'answerdate,duedate',$part)); } else { @@ -691,7 +695,17 @@ sub getDescription { } if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) && $res->handgrade($part) ne 'yes') { - return &Apache::lonhtmlcommon::direct_parm_link(&mt("Answer available"),$res->symb(),'answerdate,duedate',$part); + my $msg = &mt('Answer available'); + my $parmlist = 'answerdate,duedate'; + if (($res->is_tool) && ($res->is_gradable())) { + if (($status == $res->PARTIALLY_CORRECT) && ($res->parmval('retrypartial',$part))) { + $msg = &mt('Grade received'); + $parmlist = 'retrypartial'; + } else { + $msg = &mt('Grade available'); + } + } + return &Apache::lonhtmlcommon::direct_parm_link($msg,$res->symb(),$parmlist,$part); } if ($status == $res->EXCUSED) { return &mt("Excused by instructor"); @@ -1195,7 +1209,7 @@ sub render_quick_status { my $linkclose = ""; $result .= ''; - if ($resource->is_problem() && + if ($resource->is_gradable() && !$firstDisplayed) { my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; @@ -1220,7 +1234,7 @@ sub render_long_status { my $color; my $info = ''; - if ($resource->is_problem() || $resource->is_practice()) { + if ($resource->is_gradable() || $resource->is_practice()) { $color = $colormap{$resource->status}; if (dueInLessThan24Hours($resource, $part)) { @@ -1235,8 +1249,8 @@ sub render_long_status { } } - if ($resource->kind() eq "res" && - $resource->is_raw_problem() && + if (($resource->kind() eq "res") && + ($resource->is_raw_problem() || $resource->is_gradable()) && !$firstDisplayed) { if ($color) {$result .= ''; } $result .= getDescription($resource, $part); @@ -1283,7 +1297,7 @@ my @statuses = ($resObj->CORRECT, $resOb sub render_parts_summary_status { my ($resource, $part, $params) = @_; - if (!$resource->is_problem() && !$resource->contains_problem) { return ''; } + if (!$resource->is_gradable() && !$resource->contains_problem) { return ''; } if ($params->{showParts}) { return ''; } @@ -1705,12 +1719,32 @@ END # mark as hidden for users who have $userCanSeeHidden. # Use DFS for speed, since structure actually doesn't matter, # except what map has what resources. + # + # To ensure the "Selected Resources from selected folder in course" + # printout generation option will work in sessions launched via a + # deep link, the value of $args->{'filterFunc'} included in the + # call to lonnavmaps::render() is omitted from the filter function + # used with the DFS Iterator when $args->{'caller'} is 'printout'. + # + # As a result $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} can be + # set to 1 for folder(s) which include resources only accessible + # for sessions launched via a deep link, when the current session + # is of that type. my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap, $it->{FIRST_RESOURCE}, $it->{FINISH_RESOURCE}, {}, undef, 1); + my $dfsFilterFunc; + if ($args->{'caller'} eq 'printout') { + $dfsFilterFunc = sub { my $res = shift; return !$res->randomout() && + ($res->deeplink($args->{'caller'}) ne 'absent') && + ($res->deeplink($args->{'caller'}) ne 'grades') && + !$res->deeplinkout();}; + } else { + $dfsFilterFunc = $filterFunc; + } my $depth = 0; $dfsit->next(); my $curRes = $dfsit->next(); @@ -1729,8 +1763,9 @@ END } elsif ($curRes->src()) { # Not a sequence: if it's filtered, ignore it, otherwise # rise up the stack and mark the sequences as having children - if (&$filterFunc($curRes)) { + if (&$dfsFilterFunc($curRes)) { for my $sequence (@{$dfsit->getStack()}) { + next unless ($sequence->is_map()); $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1; } } @@ -2239,10 +2274,17 @@ sub new { $self->{USERNAME} = shift || $env{'user.name'}; $self->{DOMAIN} = shift || $env{'user.domain'}; + $self->{SECTION} = shift; $self->{CODE} = shift; - $self->{NOHIDE} = shift; + $self->{NOHIDE} = shift; + if (($self->{SECTION} eq '') && ($env{'request.course.sec'} ne '')) { + if (($self->{USERNAME} eq $env{'user.name'}) && + ($self->{USERNAME} eq $env{'user.domain'})) { + $self->{SECTION} = $env{'request.course.sec'}; + } + } # Resource cache stores navmap resources as we reference them. We generate # them on-demand so we don't pay for creating resources unless we use them. @@ -2284,7 +2326,7 @@ sub new { $self->{PARM_HASH} = \%parmhash; $self->{PARM_CACHE} = {}; } else { - $self->change_user($self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}); + $self->change_user($self->{USERNAME}, $self->{DOMAIN}, $self->{SECTION}, $self->{CODE}, $self->{NOHIDE}); } return $self; @@ -2295,15 +2337,17 @@ sub new { # username/domain associated with a navmap (e.g. to navigate for someone # else besides the current user...if sufficiently privileged. # Parameters: -# user - New user. -# domain- Domain the user belongs to. -# code - Anonymous CODE in use. +# user - New user. +# domain - Domain to which the user belongs. +# section - Section to which the user belongs. +# code - Anonymous CODE in use. # Implicit inputs: # sub change_user { my $self = shift; $self->{USERNAME} = shift; $self->{DOMAIN} = shift; + $self->{SECTION} = shift; $self->{CODE} = shift; $self->{NOHIDE} = shift; @@ -2809,7 +2853,7 @@ sub parmval_real { $self->generate_course_user_opt(); my $cid=$env{'request.course.id'}; - my $csec=$env{'request.course.sec'}; + my $csec=$self->{SECTION}; my $cgroup=''; my @cgrps=split(/:/,$env{'request.course.groups'}); if (@cgrps > 0) { @@ -2824,6 +2868,10 @@ sub parmval_real { my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); $mapname = &Apache::lonnet::deversion($mapname); + my $toolsymb = ''; + if ($fn =~ /ext\.tool$/) { + $toolsymb = $symb; + } # ----------------------------------------------------- Cascading lookup scheme my $rwhat=$what; $what=~s/^parameter\_//; @@ -2887,9 +2935,9 @@ sub parmval_real { my $meta_rwhat=$rwhat; $meta_rwhat=~s/\./_/g; - my $default=&Apache::lonnet::metadata($fn,$meta_rwhat); + my $default=&Apache::lonnet::metadata($fn,$meta_rwhat,$toolsymb); if (defined($default)) { return [$default,'resource']} - $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat); + $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat,$toolsymb); if (defined($default)) { return [$default,'resource']} # --------------------------------------------------- fifth, check more course if (defined($courseopt)) { @@ -2912,13 +2960,13 @@ sub parmval_real { if (defined($partgeneral[0])) { return \@partgeneral; } } if ($recurse) { return []; } - my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$rwhat); + my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$rwhat,$toolsymb); if (defined($pack_def)) { return [$pack_def,'resource']; } return ['']; } sub recurseup_maps { - my ($self,$mapname) = @_; + my ($self,$mapname,$getsymb) = @_; my @recurseup; if ($mapname) { my $res = $self->getResourceByUrl($mapname); @@ -2926,7 +2974,11 @@ sub recurseup_maps { my @pcs = split(/,/,$res->map_hierarchy()); shift(@pcs); if (@pcs) { - @recurseup = map { &Apache::lonnet::declutter($self->getByMapPc($_)->src()); } reverse(@pcs); + if ($getsymb) { + @recurseup = map { &Apache::lonnet::declutter($self->getByMapPc($_)->symb()); } reverse(@pcs); + } else { + @recurseup = map { &Apache::lonnet::declutter($self->getByMapPc($_)->src()); } reverse(@pcs); + } } } } @@ -3054,7 +3106,7 @@ sub get_mapparam { # Get the course id and section if there is one. my $cid=$env{'request.course.id'}; - my $csec=$env{'request.course.sec'}; + my $csec=$self->{SECTION}; my $cgroup=''; my @cgrps=split(/:/,$env{'request.course.groups'}); if (@cgrps > 0) { @@ -3223,11 +3275,11 @@ sub getcourseparam { my $uname = $self->{USERNAME}; my $udom = $self->{DOMAIN}; + my $csec = $self->{SECTION}; - # Course, section, group ids come from the env: + # Course and group ids come from the env: my $cid = $env{'request.course.id'}; - my $csec = $env{'request.course.sec'}; my $cgroup = ''; # Assume no group my @cgroups = split(/:/, $env{'request.course.groups'}); @@ -3464,6 +3516,71 @@ sub usedVersion { return $self->navhash("version_$linkurl"); } +sub isFirstResource { + my $self = shift; + my $map = shift; + my $symb = shift; + return unless (ref($map)); + my $isfirst; + my $firstResource = $map->map_start(); + if (ref($firstResource)) { + if ((!$firstResource->is_map()) && ($firstResource->src() ne '')) { + if ($firstResource->symb() eq $symb) { + $isfirst = 1; + } else { + $isfirst = 0; + } + } else { + my $it = $self->getIterator($firstResource,undef,undef,1); + while ( my $res=$it->next()) { + if ((ref($res)) && ($res->src() ne '') && (!$res->is_map())) { + if ($res->symb() eq $symb) { + $isfirst = 1; + } else { + $isfirst = 0; + } + last; + } + } + } + } + return $isfirst; +} + +sub isLastResource { + my $self = shift; + my $map = shift; + my $symb = shift; + return unless (ref($map)); + my $islast; + my $lastResource = $map->map_finish(); + if (ref($lastResource)) { + if ((!$lastResource->is_map()) && ($lastResource->src() ne '')) { + if ($lastResource->symb() eq $symb) { + $islast = 1; + } else { + $islast = 0; + } + } else { + my $currRes = $self->getBySymb($symb); + if (ref($currRes)) { + my $it = $self->getIterator($currRes,undef,undef,1); + while ( my $res=$it->next()) { + if ((ref($res)) && ($res->src() ne '') && (!$res->is_map())) { + if ($res->symb() eq $symb) { + $islast = 1; + } else { + $islast = 0; + } + last; + } + } + } + } + } + return $islast; +} + 1; package Apache::lonnavmaps::iterator; @@ -4521,6 +4638,19 @@ sub is_problem { } return 0; } +sub is_tool { + my $self=shift; + my $src = $self->src(); + return ($src =~ /ext\.tool$/); +} +sub is_gradable { + my $self=shift; + my $src = $self->src(); + if (($src =~ /$LONCAPA::assess_re/) || + (($self->is_tool()) && ($self->parmval('gradable',0) =~ /^yes$/i))) { + return !($self->is_practice()); + } +} # # The has below is the set of status that are considered 'incomplete' # @@ -5011,7 +5141,7 @@ sub weight { my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', $self->{SYMB}, $self->{DOMAIN}, $self->{USERNAME}, - $env{'request.course.sec'}); + $self->{SECTION}); return $weight; } sub part_display { @@ -5220,6 +5350,8 @@ sub parts { my $self = shift; if ($self->ext) { return []; } + if (($self->is_tool()) && + ($self->is_gradable())) { return ['0']; } $self->extractParts(); return $self->{PARTS}; @@ -5310,7 +5442,7 @@ sub extractParts { my %parts; # Retrieve part count, if this is a problem - if ($self->is_problem()) { + if ($self->is_raw_problem()) { my $partorder = &Apache::lonnet::metadata($self->src(), 'partorder'); my $metadata = &Apache::lonnet::metadata($self->src(), 'packages'); @@ -5477,7 +5609,6 @@ The problem will be opened later. Open and not yet due. - =item * B: The due date has passed, but the answer date has not yet arrived. @@ -5490,6 +5621,26 @@ The due date has passed and there is no The answer date is here. +=item * B: + +No dates have been set for this problem at all. + +=item * B: + +The due date has passed, feedback is suppressed, the problem was attempted, and the answer date has not yet arrived. + +=item * B: + +The due date has passed, feedback is suppressed, the problem was attempted, and there is no answer opening date set. + +=item * B: + +The due date has passed, feedback is suppressed, the problem was not attempted, and the answer date has not yet arrived. + +=item * B: + +The due date has passed, feedback is suppressed, the problem was not attempted, and there is no answer opening date set. + =item * B: The information is unknown due to network failure. @@ -5505,6 +5656,10 @@ 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 PAST_DUE_ATMPT_ANS { return 6; } +sub PAST_DUE_ATMPT_NOANS { return 7; } +sub PAST_DUE_NO_ATMT_ANS { return 8; } +sub PAST_DUE_NO_ATMT_NOANS { return 9; } sub NETWORK_FAILURE { return 100; } # getDateStatus gets the date status for a given problem part. @@ -5594,6 +5749,14 @@ Attempted, and not yet graded. Attempted, and credit received for attempt (survey and anonymous survey only). +=item * B: + +Attempted, but wrong for LTI Tool Provider by passback of grade + +=item * B: + +Correct for LTI Tool Provider by passback of grade + =back =cut @@ -5606,6 +5769,8 @@ sub CORRECT_BY_OVERRIDE { return 14; } sub EXCUSED { return 15; } sub ATTEMPTED { return 16; } sub CREDIT_ATTEMPTED { return 17; } +sub INCORRECT_BY_PASSBACK { return 18; } +sub CORRECT_BY_PASSBACK { return 19; } sub getCompletionStatus { my $self = shift; @@ -5620,8 +5785,12 @@ sub getCompletionStatus { if ($status eq 'correct_by_override') { return $self->CORRECT_BY_OVERRIDE; } + if ($status eq 'correct_by_passback') { + return $self->CORRECT_BY_PASSBACK; + } if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } + if ($status eq 'incorrect_by_passback') {return $self->INCORRECT_BY_PASSBACK; } if ($status eq 'excused') {return $self->EXCUSED; } if ($status eq 'ungraded_attempted') {return $self->ATTEMPTED; } if ($status eq 'credit_attempted') { @@ -5694,6 +5863,26 @@ set. The problem is past due, not considered correct, and an answer date in the future is set. +=item * B: + +The problem is past due, feedback is suppressed, the problem was +attempted and an answer date in the future is set. + +=item * B: + +The problem is past due, feedback is suppressed, the problem was +attempted and no answer date is set. + +=item * B: + +The problem is past due, feedback is suppressed, the problem was +not attempted and an answer date in the future is set. + +=item * B: + +The problem is past due, feedback is suppressed, the problem was +not attempted and no answer date is set. + =item * B: The problem is past due, not correct, and the answer is now available. @@ -5775,8 +5964,20 @@ sub status { # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || - $completionStatus == CORRECT_BY_OVERRIDE ) { - if ( $suppressFeedback ) { return ANSWER_SUBMITTED } + $completionStatus == CORRECT_BY_OVERRIDE || + $completionStatus == CORRECT_BY_PASSBACK ) { + if ( $suppressFeedback ) { + if ($dateStatus == PAST_DUE_ANSWER_LATER || + $dateStatus == PAST_DUE_NO_ANSWER ) { + if ($dateStatus == PAST_DUE_ANSWER_LATER) { + return PAST_DUE_ATMPT_ANS; + } else { + return PAST_DUE_ATMPT_NOANS; + } + } else { + return ANSWER_SUBMITTED; + } + } my $awarded=$self->awarded($part); if ($awarded < 1 && $awarded > 0) { return PARTIALLY_CORRECT; @@ -5788,7 +5989,8 @@ sub status { # If it's WRONG... and not open if ( ($completionStatus == INCORRECT || - $completionStatus == INCORRECT_BY_OVERRIDE) + $completionStatus == INCORRECT_BY_OVERRIDE || + $completionStatus == INCORRECT_BY_PASSBACK) && (!$self->opendate($part) || $self->opendate($part) > time()) ) { return INCORRECT; } @@ -5815,7 +6017,23 @@ sub status { if ($dateStatus == PAST_DUE_ANSWER_LATER || $dateStatus == PAST_DUE_NO_ANSWER ) { - return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus; + if ($suppressFeedback) { + if ($completionStatus == NOT_ATTEMPTED) { + if ($dateStatus == PAST_DUE_ANSWER_LATER) { + return PAST_DUE_NO_ATMT_ANS; + } else { + return PAST_DUE_NO_ATMT_NOANS; + } + } else { + if ($dateStatus == PAST_DUE_ANSWER_LATER) { + return PAST_DUE_ATMPT_ANS; + } else { + return PAST_DUE_ATMPT_NOANS; + } + } + } else { + return $dateStatus; + } } if ($dateStatus == ANSWER_OPEN) { @@ -5830,7 +6048,8 @@ sub status { } # If it's WRONG... - if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { + if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE || + $completionStatus == INCORRECT_BY_PASSBACK) { # and there are TRIES LEFT: if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { return $suppressFeedback ? ANSWER_SUBMITTED : TRIES_LEFT; @@ -6025,6 +6244,10 @@ my %compositeToSimple = EXCUSED() => CORRECT, PAST_DUE_NO_ANSWER() => INCORRECT, PAST_DUE_ANSWER_LATER() => INCORRECT, + PAST_DUE_ATMPT_ANS() => ATTEMPTED, + PAST_DUE_ATMPT_NOANS() => ATTEMPTED, + PAST_DUE_NO_ATMT_ANS() => CLOSED, + PAST_DUE_NO_ATMT_NOANS() => CLOSED, ANSWER_OPEN() => INCORRECT, OPEN_LATER() => CLOSED, TRIES_LEFT() => OPEN,