--- loncom/interface/lonnavmaps.pm 2015/04/13 16:46:43 1.506 +++ loncom/interface/lonnavmaps.pm 2021/12/31 23:14:04 1.509.2.14 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.506 2015/04/13 16:46:43 raeburn Exp $ +# $Id: lonnavmaps.pm,v 1.509.2.14 2021/12/31 23:14:04 raeburn Exp $ # # Copyright Michigan State University Board of Trustees @@ -486,7 +486,7 @@ use Apache::lonlocal; use Apache::lonnet; use Apache::lonmap; -use POSIX qw (floor strftime); +use POSIX qw (ceil floor strftime); use Time::HiRes qw( gettimeofday tv_interval ); use LONCAPA; use DateTime(); @@ -577,7 +577,11 @@ sub getLinkForResource { my $anchor; if ($res->is_page()) { foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } - $anchor=&escape($anchor->shown_symb()); + if ($anchor->encrypted() && !&advancedUser()) { + $anchor='LC_'.$anchor->id(); + } else { + $anchor=&escape($anchor->shown_symb()); + } return ($res->link(),$res->shown_symb(),$anchor); } # in case folder was skipped over as "only sequence" @@ -624,44 +628,55 @@ sub getDescription { if ($status == $res->OPEN_LATER) { return &mt("Open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($open,'start'),$res->symb(),'opendate',$part)); } + my $slotinfo; if ($res->simpleStatus($part) == $res->OPEN) { unless (&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) { my ($slot_status,$slot_time,$slot_name)=$res->check_for_slot($part); + my $slotmsg; if ($slot_status == $res->UNKNOWN) { - return &mt('Reservation status unknown'); + $slotmsg = &mt('Reservation status unknown'); } elsif ($slot_status == $res->RESERVED) { - return &mt('Reserved - ends [_1]', + $slotmsg = &mt('Reserved - ends [_1]', timeToHumanString($slot_time,'end')); } elsif ($slot_status == $res->RESERVED_LOCATION) { - return &mt('Reserved - specific location(s) - ends [_1]', + $slotmsg = &mt('Reserved - specific location(s) - ends [_1]', timeToHumanString($slot_time,'end')); } elsif ($slot_status == $res->RESERVED_LATER) { - return &mt('Reserved - next open [_1]', + $slotmsg = &mt('Reserved - next open [_1]', timeToHumanString($slot_time,'start')); } elsif ($slot_status == $res->RESERVABLE) { - return &mt('Reservable, reservations close [_1]', + $slotmsg = &mt('Reservable, reservations close [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->NEEDS_CHECKIN) { + $slotmsg = &mt('Reserved, check-in needed - ends [_1]', timeToHumanString($slot_time,'end')); } elsif ($slot_status == $res->RESERVABLE_LATER) { - return &mt('Reservable, reservations open [_1]', + $slotmsg = &mt('Reservable, reservations open [_1]', timeToHumanString($slot_time,'start')); } elsif ($slot_status == $res->NOT_IN_A_SLOT) { - return &mt('Reserve a time/place to work'); + $slotmsg = &mt('Reserve a time/place to work'); } elsif ($slot_status == $res->NOTRESERVABLE) { - return &mt('Reservation not available'); + $slotmsg = &mt('Reservation not available'); } elsif ($slot_status == $res->WAITING_FOR_GRADE) { - return &mt('Submission in grading queue'); + $slotmsg = &mt('Submission in grading queue'); + } + if ($slotmsg) { + if ($res->is_task() || !$due) { + return $slotmsg; + } + $slotinfo = (' ' x 2).'('.$slotmsg.')'; } } } if ($status == $res->OPEN) { if ($due) { if ($res->is_practice()) { - return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)); + return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)).$slotinfo; } else { - return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)); + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)).$slotinfo; } } else { - return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part); + return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part).$slotinfo; } } if ($status == $res->PAST_DUE_ANSWER_LATER) { @@ -907,6 +922,9 @@ sub render_resource { my $nonLinkedText = ''; # stuff after resource title not in link my $link = $params->{"resourceLink"}; + if ($resource->ext()) { + $link =~ s/\#.+(\?)/$1/g; + } # The URL part is not escaped at this point, but the symb is... @@ -927,7 +945,28 @@ sub render_resource { # links to open and close the folder my $whitespace = $location.'/whitespace_21.gif'; - my $linkopen = "".""; + my $linkopen = ""; + my $nomodal; + if (($params->{'modalLink'}) && (!$resource->is_sequence())) { + if ($link =~m{^(?:|/adm/wrapper)/ext/([^#]+)}) { + my $exturl = $1; + if (($ENV{'SERVER_PORT'} == 443) && ($exturl !~ /^https:/)) { + $nomodal = 1; + } + } elsif (($link eq "/public/$LONCAPA::match_domain/$LONCAPA::match_courseid/syllabus") && + ($env{'request.course.id'}) && ($ENV{'SERVER_PORT'} == 443) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + $nomodal = 1; + } + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen .= ""; + } else { + $linkopen .= ""; + } + } else { + $linkopen .= ""; + } my $linkclose = ""; # Default icon: unknown page @@ -981,11 +1020,15 @@ sub render_resource { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; $icon = ""."\"".($nowOpen"; - - $linkopen = ""; - $linkclose = ""; + if ($params->{'caller'} eq 'sequence') { + $linkopen = ""; + } else { + $linkopen = ""; + $linkclose = ""; + } } - if ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) && + if (((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) || + (&Apache::lonnet::allowed('cev',$env{'request.course.id'}))) && ($resource->symb=~/\_\_\_[^\_]+\_\_\_uploaded/)) { if (!$params->{'map_no_edit_link'}) { my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png'; @@ -995,10 +1038,13 @@ sub render_resource { ''; } } - } - - if ($resource->randomout()) { - $nonLinkedText .= ' ('.&mt('hidden').') '; + if ($params->{'mapHidden'} || $resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } + } else { + if ($resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } } if (!$resource->condval()) { $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; @@ -1048,10 +1094,19 @@ sub render_resource { } if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { - $result .= "$curMarkerBegin$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; - } else { - $result .= "$curMarkerBegin$linkopen$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; + $linkclose = ''; + if ($params->{'modalLink'}) { + my $esclink = &js_escape($link); + if ($nomodal) { + $linkopen = ""; + } else { + $linkopen = ""; + } + } else { + $linkopen = ""; + } } + $result .= "$curMarkerBegin$linkopen$title$partLabel$linkclose$curMarkerEnd$editmapLink$nonLinkedText"; return $result; } @@ -1342,10 +1397,11 @@ sub render { my $currenturl = $env{'form.postdata'}; #$currenturl=~s/^http\:\/\///; #$currenturl=~s/^[^\/]+//; - - $here = $jump = &Apache::lonnet::symbread($currenturl); + unless ($args->{'caller'} eq 'sequence') { + $here = $jump = &Apache::lonnet::symbread($currenturl); + } } - if ($here eq '') { + if (($here eq '') && ($args->{'caller'} ne 'sequence')) { my $last; if (tie(my %hash,'GDBM_File',$env{'request.course.fn'}.'_symb.db', &GDBM_READER(),0640)) { @@ -1405,10 +1461,13 @@ sub render { if ($args->{'iterator_map'}) { my $map = $args->{'iterator_map'}; $map = $navmap->getResourceByUrl($map); - my $firstResource = $map->map_start(); - my $finishResource = $map->map_finish(); - - $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + if (ref($map)) { + my $firstResource = $map->map_start(); + my $finishResource = $map->map_finish(); + $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); + } else { + return; + } } else { $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'}); } @@ -1527,7 +1586,8 @@ END $result.=''; } if (($args->{'caller'} eq 'navmapsdisplay') && - (&Apache::lonnet::allowed('mdc',$env{'request.course.id'}))) { + ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) || + (&Apache::lonnet::allowed('cev',$env{'request.course.id'})))) { my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; if ($env{'course.'.$env{'request.course.id'}.'.url'} eq @@ -1573,41 +1633,46 @@ END $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); - my $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_map()) { - $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; - } + # If we're suppressing empty sequences, look for them here. + # We also do this even if $args->{'suppressEmptySequences'} + # is not true, so we can hide empty sequences for which the + # hiddenresource parameter is set to yes (at map level), or + # mark as hidden for users who have $userCanSeeHidden. + # Use DFS for speed, since structure actually doesn't matter, + # except what map has what resources. + + my $dfsit = Apache::lonnavmaps::DFSiterator->new($navmap, + $it->{FIRST_RESOURCE}, + $it->{FINISH_RESOURCE}, + {}, undef, 1); + + my $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_map()) { + $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. + } 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)) { + for my $sequence (@{$dfsit->getStack()}) { + $sequence->{DATA}->{HAS_VISIBLE_CHILDREN} = 1; } } } - } continue { - $curRes = $dfsit->next(); } + } continue { + $curRes = $dfsit->next(); } my $displayedJumpMarker = 0; @@ -1665,6 +1730,28 @@ END undef($args->{'sort'}); } + # Determine if page will be served with https in case + # it contains a syllabus which uses an external URL + # which points at an http site. + + my ($is_ssl,$cdom,$cnum,$hostname); + if ($ENV{'SERVER_PORT'} == 443) { + $is_ssl = 1; + if ($r) { + $hostname = $r->hostname(); + } else { + $hostname = $ENV{'SERVER_NAME'}; + } + } + if ($env{'request.course.id'}) { + $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + } + + my $inhibitmenu; + if ($args->{'modalLink'}) { + $inhibitmenu = '&inhibitmenu=yes'; + } while (1) { if ($args->{'sort'}) { @@ -1700,9 +1787,21 @@ END } # If this is an empty sequence and we're filtering them, continue on - if ($curRes->is_map() && $args->{'suppressEmptySequences'} && - !$curRes->{DATA}->{HAS_VISIBLE_CHILDREN}) { - next; + $args->{'mapHidden'} = 0; + if (($curRes->is_map()) && (!$curRes->{DATA}->{HAS_VISIBLE_CHILDREN})) { + if ($args->{'suppressEmptySequences'}) { + next; + } else { + my $mapname = &Apache::lonnet::declutter($curRes->src()); + $mapname = &Apache::lonnet::deversion($mapname); + if (lc($navmap->get_mapparam(undef,$mapname,"0.hiddenresource")) eq 'yes') { + if ($userCanSeeHidden) { + $args->{'mapHidden'} = 1; + } else { + next; + } + } + } } # If we're suppressing navmaps and this is a navmap, continue on @@ -1786,11 +1885,35 @@ END $stack=$it->getStack(); } ($src,$symb,$anchor)=getLinkForResource($stack); + my $srcHasQuestion = $src =~ /\?/; + if ($env{'request.course.id'}) { + if (($is_ssl) && ($src =~ m{^\Q/public/$cdom/$cnum/syllabus\E($|\?)}) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } elsif (($is_ssl) && ($src =~ m{^\Q/adm/wrapper/ext/\E(?!https:)})) { + unless ((&Apache::lonnet::uses_sts()) || (&Apache::lonnet::waf_allssl($hostname))) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } + } + } if (defined($anchor)) { $anchor='#'.$anchor; } - my $srcHasQuestion = $src =~ /\?/; - $args->{"resourceLink"} = $src. - ($srcHasQuestion?'&':'?') . - 'symb=' . &escape($symb).$anchor; + if (($args->{'caller'} eq 'sequence') && ($curRes->is_map())) { + $args->{"resourceLink"} = $src.($srcHasQuestion?'&':'?') .'navmap=1'; + } else { + $args->{"resourceLink"} = $src. + ($srcHasQuestion?'&':'?') . + 'symb=' . &escape($symb).$inhibitmenu.$anchor; + } } # Now, we've decided what parts to show. Loop through them and # show them. @@ -1818,7 +1941,7 @@ END $currentJumpDelta) { # Jam the anchor after the tag; # necessary for valid HTML (which Mozilla requires) - $colHTML =~ s/\>/\>\/; + $colHTML =~ s/\>/\>\\<\/a\>/; $displayedJumpMarker = 1; } $result .= $colHTML . "\n"; @@ -2204,7 +2327,7 @@ sub generate_email_discuss_status { foreach my $msgid (@keys) { if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { my ($sendtime,$shortsubj,$fromname,$fromdomain,$status,$fromcid, - $symb,$error) = &Apache::lonmsg::unpackmsgid($msgid); + $symb,$error) = &Apache::lonmsg::unpackmsgid(&LONCAPA::escape($msgid)); &Apache::lonenc::check_decrypt(\$symb); if (($fromcid ne '') && ($fromcid ne $cid)) { next; @@ -2701,6 +2824,108 @@ sub parmval_real { if (defined($pack_def)) { return [$pack_def,'resource']; } return ['']; } + +sub recurseup_maps { + my ($self,$mapname) = @_; + my @recurseup; + if ($mapname) { + my $res = $self->getResourceByUrl($mapname); + if (ref($res)) { + my @pcs = split(/,/,$res->map_hierarchy()); + shift(@pcs); + if (@pcs) { + @recurseup = map { &Apache::lonnet::declutter($self->getByMapPc($_)->src()); } reverse(@pcs); + } + } + } + return @recurseup; +} + +sub recursed_crumbs { + my ($self,$mapurl,$restitle) = @_; + my (@revmapinfo,@revmapres); + my $mapres = $self->getResourceByUrl($mapurl); + if (ref($mapres)) { + @revmapres = map { $self->getByMapPc($_); } split(/,/,$mapres->map_breadcrumbs()); + shift(@revmapres); + } + my $allowedlength = 60; + my $minlength = 5; + my $allowedtitle = 30; + if (($env{'environment.icons'} eq 'iconsonly') && (!$env{'browser.mobile'})) { + $allowedlength = 100; + $allowedtitle = 70; + } + if (length($restitle) > $allowedtitle) { + $restitle = &truncate_crumb_text($restitle,$allowedtitle); + } + my $totallength = length($restitle); + my @links; + + foreach my $map (@revmapres) { + my $pc = $map->map_pc(); + next if ((!$pc) || ($pc == 1)); + push(@links,$map); + push(@revmapinfo,{'href' => $env{'request.use_absolute'}.$map->link().'?navmap=1','text' => $map->title(),'no_mt' => 1,}); + $totallength += length($map->title()); + } + my $numlinks = scalar(@links); + if ($numlinks) { + if ($totallength - $allowedlength > 0) { + my $available = $allowedlength - length($restitle); + my $avg = POSIX::ceil($available/$numlinks); + if ($avg < $minlength) { + $avg = $minlength; + } + @revmapinfo = (); + foreach my $map (@links) { + my $showntitle = &truncate_crumb_text($map->title(),$avg); + if ($showntitle ne '') { + push(@revmapinfo,{'href' => $env{'request.use_absolute'}.$map->link().'?navmap=1','text' => $showntitle,'no_mt' => 1,}); + } + } + } + } + if ($restitle ne '') { + push(@revmapinfo,{'text' => $restitle, 'no_mt' => 1}); + } + return @revmapinfo; +} + +sub truncate_crumb_text { + my ($title,$limit) = @_; + my $showntitle = ''; + if (length($title) > $limit) { + my @words = split(/\b\s*/,$title); + if (@words == 1) { + $showntitle = substr($title,0,$limit).' ...'; + } else { + my $linklength = 0; + my $num = 0; + foreach my $word (@words) { + $linklength += 1+length($word); + if ($word eq '-') { + $showntitle =~ s/ $//; + $showntitle .= $word; + } elsif ($linklength > $limit) { + if ($num < @words) { + $showntitle .= $word.' ...'; + last; + } else { + $showntitle .= $word; + } + } else { + $showntitle .= $word.' '; + } + } + $showntitle =~ s/ $//; + } + return $showntitle; + } else { + return $title; + } +} + # # Determines the open/close dates for printing a map that # encloses a resource. @@ -2712,15 +2937,15 @@ sub map_printdates { - my $opendate = $self->get_mapparam($res->symb(), "$part.printstartdate"); - my $closedate= $self->get_mapparam($res->symb(), "$part.printenddate"); + my $opendate = $self->get_mapparam($res->symb(),'',"$part.printstartdate"); + my $closedate= $self->get_mapparam($res->symb(),'', "$part.printenddate"); return ($opendate, $closedate); } sub get_mapparam { - my ($self, $symb, $what) = @_; + my ($self, $symb, $mapname, $what) = @_; # Ensure the course option hash is populated: @@ -2739,15 +2964,17 @@ sub get_mapparam { my $uname=$self->{USERNAME}; my $udom=$self->{DOMAIN}; - unless ($symb) { return ['']; } + unless ($symb || $mapname) { return; } my $result=''; - + my ($recursed,@recurseup); # Figure out which map we are in. - my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); - $mapname = &Apache::lonnet::deversion($mapname); - + if ($symb && !$mapname) { + my ($id,$fn); + ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); + $mapname = &Apache::lonnet::deversion($mapname); + } my $rwhat=$what; $what=~s/^parameter\_//; @@ -2779,6 +3006,20 @@ sub get_mapparam { if (defined($$useropt{$courselevel})) { return $$useropt{$courselevel}; } + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + unless ($recursed) { + @recurseup = $self->recurseup_maps($mapname); + $recursed = 1; + } + foreach my $item (@recurseup) { + my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what; + if (defined($$useropt{$norecursechk})) { + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + return $$useropt{$norecursechk}; + } + } + } + } } # Check course -- group @@ -2789,6 +3030,20 @@ sub get_mapparam { if (defined($$courseopt{$grplevel})) { return $$courseopt{$grplevel}; } + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + unless ($recursed) { + @recurseup = $self->recurseup_maps($mapname); + $recursed = 1; + } + foreach my $item (@recurseup) { + my $norecursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(all).'.$what; + if (defined($$courseopt{$norecursechk})) { + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + return $$courseopt{$norecursechk}; + } + } + } + } } # Check course -- section @@ -2801,12 +3056,29 @@ sub get_mapparam { if (defined($$courseopt{$seclevel})) { return $$courseopt{$seclevel}; } + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + unless ($recursed) { + @recurseup = $self->recurseup_maps($mapname); + $recursed = 1; + } + foreach my $item (@recurseup) { + my $norecursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(all).'.$what; + if (defined($$courseopt{$norecursechk})) { + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + return $$courseopt{$norecursechk}; + } + } + } + } } # Check the map parameters themselves: - my $thisparm = $$parmhash{$symbparm}; - if (defined($thisparm)) { - return $thisparm; + if ($symb) { + my $symbparm=$symb.'.'.$what; + my $thisparm = $$parmhash{$symbparm}; + if (defined($thisparm)) { + return $thisparm; + } } @@ -2816,6 +3088,20 @@ sub get_mapparam { if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + unless ($recursed) { + @recurseup = $self->recurseup_maps($mapname); + $recursed = 1; + } + foreach my $item (@recurseup) { + my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what; + if (defined($$courseopt{$norecursechk})) { + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + return $$courseopt{$norecursechk}; + } + } + } + } } return undef; # Unefined if we got here. } @@ -2871,7 +3157,7 @@ sub getcourseparam { # # We want the course level stuff from the way # parmval_real operates - # TODO: Fator some of this stuff out of + # TODO: Factor some of this stuff out of # both parmval_real and here # my $courselevel = $cid . '.' . $what; @@ -2888,7 +3174,7 @@ sub getcourseparam { } # Try for the group's course level option: - if ($uname ne '' and defined($courseopt)) { + if ($cgroup ne '' and defined($courseopt)) { if (defined($$courseopt{$grplevel})) { return $$courseopt{$grplevel}; } @@ -2896,12 +3182,12 @@ sub getcourseparam { # Try for section level parameters: - if ($csec and defined($courseopt)) { + if ($csec ne '' and defined($courseopt)) { if (defined($$courseopt{$seclevel})) { return $$courseopt{$seclevel}; } } - # Try for 'additional' course parameterse: + # Try for 'additional' course parameters: if (defined($courseopt)) { if (defined($$courseopt{$courselevel})) { @@ -3513,7 +3799,8 @@ sub next { # That ends the main iterator logic. Now, do we want to recurse # down this map (if this resource is a map)? if ( ($self->{HERE}->is_sequence() || (!$closeAllPages && $self->{HERE}->is_page())) && - (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { + (defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION}) && + ($env{'request.role.adv'} || !$self->{HERE}->randomout())) { $self->{RECURSIVE_ITERATOR_FLAG} = 1; my $firstResource = $self->{HERE}->map_start(); my $finishResource = $self->{HERE}->map_finish(); @@ -3860,9 +4147,12 @@ sub new { # about this resource in. Not used by the resource object # directly. $self->{DATA} = {}; - + bless($self); + # This is a speed optimization, to avoid calling symb() too often. + $self->{SYMB} = $self->symb(); + return $self; } @@ -3974,8 +4264,8 @@ sub src { } sub shown_symb { my $self=shift; - if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->symb());} - return $self->symb(); + if ($self->encrypted()) {return &Apache::lonenc::encrypted($self->{SYMB});} + return $self->{SYMB}; } sub id { my $self=shift; @@ -3988,6 +4278,7 @@ sub enclosing_map_src { } sub symb { my $self=shift; + if (defined($self->{SYMB})) { return $self->{SYMB}; } (my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; my $symbSrc = &Apache::lonnet::declutter($self->src()); my $symb = &Apache::lonnet::declutter($self->navHash('map_id_'.$first)) @@ -3996,7 +4287,7 @@ sub symb { } sub wrap_symb { my $self = shift; - return $self->{NAV_MAP}->wrap_symb($self->symb()); + return $self->{NAV_MAP}->wrap_symb($self->{SYMB}); } sub title { my $self=shift; @@ -4219,7 +4510,7 @@ sub parmval { if (!defined($part)) { $part = '0'; } - return $self->{NAV_MAP}->parmval($part.'.'.$what, $self->symb()); + return $self->{NAV_MAP}->parmval($part.'.'.$what, $self->{SYMB}); } =pod @@ -4258,6 +4549,12 @@ Returns a string with a comma-separated for the hierarchy of maps containing a map, with the top level map first, then descending to deeper levels, with the enclosing map last. +=item * B: + +Same as map_hierarchy, except maps containing only a single itemm if +it's a map, or containing no items are omitted, unless it's the top +level map (map_pc = 1), which is always included. + =back =cut @@ -4293,6 +4590,11 @@ sub map_hierarchy { my $pc = $self->map_pc(); return $self->navHash("map_hierarchy_$pc", 0); } +sub map_breadcrumbs { + my $self = shift; + my $pc = $self->map_pc(); + return $self->navHash("map_breadcrumbs_$pc", 0); +} ##### # Property queries @@ -4472,25 +4774,25 @@ sub awarded { my $self = shift; my $part = shift; $self->{NAV_MAP}->get_user_data(); if (!defined($part)) { $part = '0'; } - return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.awarded'}; + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.awarded'}; } sub taskversion { my $self = shift; my $part = shift; $self->{NAV_MAP}->get_user_data(); if (!defined($part)) { $part = '0'; } - return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.version'}; + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.version'}; } sub taskstatus { my $self = shift; my $part = shift; $self->{NAV_MAP}->get_user_data(); if (!defined($part)) { $part = '0'; } - return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$self->taskversion($part).'.'.$part.'.status'}; + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$self->taskversion($part).'.'.$part.'.status'}; } sub solved { my $self = shift; my $part = shift; $self->{NAV_MAP}->get_user_data(); if (!defined($part)) { $part = '0'; } - return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.solved'}; + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.solved'}; } sub checkedin { my $self = shift; my $part = shift; @@ -4498,9 +4800,9 @@ sub checkedin { if (!defined($part)) { $part = '0'; } if ($self->is_task()) { my $version = $self->taskversion($part); - return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$version .'.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$version .'.'.$part.'.checkedin.slot'}); + return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$version .'.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$version .'.'.$part.'.checkedin.slot'}); } else { - return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.checkedin.slot'}); + return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}->{'resource.'.$part.'.checkedin.slot'}); } } # this should work exactly like the copy in lonhomework.pm @@ -4519,7 +4821,7 @@ sub duedate { my $due_date=$self->parmval("duedate", $part); if ($interval[0] =~ /\d+/) { my $first_access=&Apache::lonnet::get_first_access($interval[1], - $self->symb); + $self->{SYMB}); if (defined($first_access)) { my $interval = $first_access+$interval[0]; $date = (!$due_date || $interval < $due_date) ? $interval @@ -4592,7 +4894,7 @@ sub weight { my $self = shift; my $part = shift; if (!defined($part)) { $part = '0'; } my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', - $self->symb(), $self->{DOMAIN}, + $self->{SYMB}, $self->{DOMAIN}, $self->{USERNAME}, $env{'request.course.sec'}); return $weight; @@ -4601,7 +4903,7 @@ sub part_display { my $self= shift(); my $partID = shift(); if (! defined($partID)) { $partID = '0'; } my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display', - $self->symb); + $self->{SYMB}); if (! defined($display) || $display eq '') { $display = $partID; } @@ -4621,7 +4923,7 @@ sub getReturnHash { my $self = shift; if (!defined($self->{RETURN_HASH})) { - my %tmpHash = &Apache::lonnet::restore($self->symb(),undef,$self->{DOMAIN},$self->{USERNAME}); + my %tmpHash = &Apache::lonnet::restore($self->{SYMB},undef,$self->{DOMAIN},$self->{USERNAME}); $self->{RETURN_HASH} = \%tmpHash; } } @@ -4686,23 +4988,23 @@ and use the link as appropriate. sub hasDiscussion { my $self = shift; - return $self->{NAV_MAP}->hasDiscussion($self->symb()); + return $self->{NAV_MAP}->hasDiscussion($self->{SYMB}); } sub last_post_time { my $self = shift; - return $self->{NAV_MAP}->last_post_time($self->symb()); + return $self->{NAV_MAP}->last_post_time($self->{SYMB}); } sub discussion_info { my ($self,$filter) = @_; - return $self->{NAV_MAP}->discussion_info($self->symb(),$filter); + return $self->{NAV_MAP}->discussion_info($self->{SYMB},$filter); } sub getFeedback { my $self = shift; my $source = $self->src(); - my $symb = $self->symb(); + my $symb = $self->{SYMB}; if ($source =~ /^\/res\//) { $source = substr $source, 5; } return $self->{NAV_MAP}->getFeedback($symb,$source); } @@ -4710,7 +5012,7 @@ sub getFeedback { sub getErrors { my $self = shift; my $source = $self->src(); - my $symb = $self->symb(); + my $symb = $self->{SYMB}; if ($source =~ /^\/res\//) { $source = substr $source, 5; } return $self->{NAV_MAP}->getErrors($symb,$source); } @@ -4860,7 +5162,7 @@ sub extractParts { if ($partorder) { my @parts; for my $part (split (/,/,$partorder)) { - if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { + if (!Apache::loncommon::check_if_partid_hidden($part, $self->{SYMB})) { push @parts, $part; $parts{$part} = 1; } @@ -4878,12 +5180,12 @@ sub extractParts { my $part = $1; # This floods the logs if it blows up if (defined($parts{$part})) { - &Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->symb()); + &Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->{SYMB}); } # check to see if part is turned off. - if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { + if (!Apache::loncommon::check_if_partid_hidden($part, $self->{SYMB})) { $parts{$part} = 1; } } @@ -5388,7 +5690,7 @@ sub status { sub check_for_slot { my $self = shift; my $part = shift; - my $symb = $self->symb(); + my $symb = $self->{SYMB}; my ($use_slots,$available,$availablestudent) = $self->slot_control($part); if (($use_slots ne '') && ($use_slots !~ /^\s*no\s*$/i)) { my @slots = (split(/:/,$availablestudent),split(/:/,$available)); @@ -5397,13 +5699,13 @@ sub check_for_slot { my $cnum=$env{'course.'.$cid.'.num'}; my $now = time; my $num_usable_slots = 0; + my ($checkedin,$checkedinslot,%consumed_uniq,%slots); if (@slots > 0) { - my %slots=&Apache::lonnet::get('slots',[@slots],$cdom,$cnum); + %slots=&Apache::lonnet::get('slots',[@slots],$cdom,$cnum); if (&Apache::lonnet::error(%slots)) { return (UNKNOWN); } my @sorted_slots = &Apache::loncommon::sorted_slots(\@slots,\%slots,'starttime'); - my ($checkedin,$checkedinslot); foreach my $slot_name (@sorted_slots) { next if (!defined($slots{$slot_name}) || !ref($slots{$slot_name})); my $end = $slots{$slot_name}->{'endtime'}; @@ -5427,7 +5729,7 @@ sub check_for_slot { ($checkedin,$checkedinslot) = $self->checkedin(); unless ((grep(/^\Q$checkedin\E/,@proctors)) && ($checkedinslot eq $slot_name)) { - return (NEEDS_CHECKIN,undef,$slot_name); + return (NEEDS_CHECKIN,$end,$slot_name); } } return (RESERVED,$end,$slot_name); @@ -5437,19 +5739,30 @@ sub check_for_slot { $num_usable_slots ++; } } - my ($is_correct,$got_grade); + my ($is_correct,$wait_for_grade); if ($self->is_task()) { my $taskstatus = $self->taskstatus(); $is_correct = (($taskstatus eq 'pass') || ($self->solved() =~ /^correct_/)); - $got_grade = ($taskstatus =~ /^(?:pass|fail)$/); + unless ($taskstatus =~ /^(?:pass|fail)$/) { + $wait_for_grade = 1; + } } else { - $got_grade = 1; - $is_correct = ($self->solved() =~ /^correct_/); + unless ($self->completable()) { + $wait_for_grade = 1; + } + unless (($self->problemstatus($part) eq 'no') || + ($self->problemstatus($part) eq 'no_feedback_ever')) { + $is_correct = ($self->solved($part) =~ /^correct_/); + $wait_for_grade = 0; + } } ($checkedin,$checkedinslot) = $self->checkedin(); if ($checkedin) { - if (!$got_grade) { + if (ref($slots{$checkedinslot}) eq 'HASH') { + $consumed_uniq{$checkedinslot} = $slots{$checkedinslot}{'uniqueperiod'}; + } + if ($wait_for_grade) { return (WAITING_FOR_GRADE); } elsif ($is_correct) { return (CORRECT); @@ -5464,16 +5777,46 @@ sub check_for_slot { if (ref($reservable) eq 'HASH') { if ((ref($reservable->{'now_order'}) eq 'ARRAY') && (ref($reservable->{'now'}) eq 'HASH')) { foreach my $slot (reverse (@{$reservable->{'now_order'}})) { + my $canuse; if (($reservable->{'now'}{$slot}{'symb'} eq '') || ($reservable->{'now'}{$slot}{'symb'} eq $symb)) { + $canuse = 1; + } + if ($canuse) { + if ($checkedin) { + if (ref($consumed_uniq{$checkedinslot}) eq 'ARRAY') { + my ($uniqstart,$uniqend)=@{$consumed_uniq{$checkedinslot}}; + if ($reservable->{'now'}{$slot}{'uniqueperiod'} =~ /^(\d+),(\d+)$/) { + my ($new_uniq_start,$new_uniq_end) = ($1,$2); + next if (! + ($uniqstart < $new_uniq_start && $uniqend < $new_uniq_start) || + ($uniqstart > $new_uniq_end && $uniqend > $new_uniq_end )); + } + } + } return(RESERVABLE,$reservable->{'now'}{$slot}{'endreserve'}); } } } if ((ref($reservable->{'future_order'}) eq 'ARRAY') && (ref($reservable->{'future'}) eq 'HASH')) { foreach my $slot (@{$reservable->{'future_order'}}) { + my $canuse; if (($reservable->{'future'}{$slot}{'symb'} eq '') || ($reservable->{'future'}{$slot}{'symb'} eq $symb)) { + $canuse = 1; + } + if ($canuse) { + if ($checkedin) { + if (ref($consumed_uniq{$checkedinslot}) eq 'ARRAY') { + my ($uniqstart,$uniqend)=@{$consumed_uniq{$checkedinslot}}; + if ($reservable->{'future'}{$slot}{'uniqueperiod'} =~ /^(\d+),(\d+)$/) { + my ($new_uniq_start,$new_uniq_end) = ($1,$2); + next if (! + ($uniqstart < $new_uniq_start && $uniqend < $new_uniq_start) || + ($uniqstart > $new_uniq_end && $uniqend > $new_uniq_end )); + } + } + } return(RESERVABLE_LATER,$reservable->{'future'}{$slot}{'startreserve'}); } } @@ -5624,6 +5967,39 @@ sub completable { =pod +B + +The answerable method differs from the completable method in its handling of problem parts +for which feedback on correctness is suppressed, but the student still has tries left, and +the problem part is not past due, (i.e., the student could submit a different answer if +he/she so chose). For that case completable will return 0, whereas answerable will return 1. + +=cut + +sub answerable { + 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 ($self->getCompletionStatus($part) == ATTEMPTED() || + $self->getCompletionStatus($part) == CREDIT_ATTEMPTED() || + $status == ANSWER_SUBMITTED() ) { + if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { + return 1; + } + } + if ($status == OPEN() || $status == TRIES_LEFT() || $status == NETWORK_FAILURE()) { + return 1; + } + } + # None of the parts were answerable, so neither is this problem. + return 0; +} + +=pod + =head2 Resource/Nav Map Navigation =over 4 @@ -5677,7 +6053,7 @@ sub browsePriv { } $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre',$self->src(), - $self->symb(),undef, + $self->{SYMB},undef, undef,$noblockcheck); }