--- 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,