--- loncom/interface/lonnavmaps.pm 2016/03/02 00:51:15 1.513 +++ loncom/interface/lonnavmaps.pm 2017/09/13 23:35:07 1.536 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.513 2016/03/02 00:51:15 raeburn Exp $ +# $Id: lonnavmaps.pm,v 1.536 2017/09/13 23:35:07 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -51,9 +51,16 @@ described at http://www.lon-capa.org. X When a user enters a course, LON-CAPA examines the course structure and caches it in what is often referred to as the "big hash" X. You can see it if you are logged into -LON-CAPA, in a course, by going to /adm/test. (You may need to -tweak the /home/httpd/lonTabs/htpasswd file to view it.) The -content of the hash will be under the heading "Big Hash". +LON-CAPA, in a course, by going to /adm/test. The content of +the hash will be under the heading "Big Hash". + +Access to /adm/test is controlled by a domain configuration, +which a Domain Coordinator will set for a server's default domain +via: Main Menu > Set domain configuration > Display (Access to +server status pages checked), and entering a username:domain +or IP address in the "Show user environment" row. Users with +an unexpired domain coordinator role in the server's domain +automatically receive access to /adm/test. Big Hash contains, among other things, how resources are related to each other (next/previous), what resources are maps, which @@ -77,11 +84,18 @@ Apache::lonnavmaps also provides fairly rendering navmaps, and last but not least, provides the navmaps view for when the user clicks the NAV button. -B: Apache::lonnavmaps I works for the "currently -logged in user"; if you want things like "due dates for another -student" lonnavmaps can not directly retrieve information like -that. You need the EXT function. This module can still help, -because many things, such as the course structure, are constant +B: Apache::lonnavmaps by default will show information +for the "currently logged in user". However, if information +about resources is needed for a different user, e.g., a bubblesheet +exam which uses randomorder, or randompick needs to be printed or +graded for named user(s) or specific CODEs, then the username, +domain, or CODE can be passed as arguments when creating a new +navmap object. + +Note if you want things like "due dates for another student, +you would use the EXT function instead of lonnavmaps. +That said, the lonnavmaps module can still help, because many +things, such as the course structure, are usually constant between users, and Apache::lonnavmaps can help by providing symbs for the EXT call. @@ -91,7 +105,9 @@ all, then documents the Apache::lonnavma is the key to accessing the Big Hash information, covers the use of the Iterator (which provides the logic for traversing the somewhat-complicated Big Hash data structure), documents the -Apache::lonnavmaps::Resource objects that are returned by +Apache::lonnavmaps::Resource objects that are returned singularly +by: getBySymb(), getById(), getByMapPc(), and getResourceByUrl() +(can also be as an array), or in an array by retrieveResources(). =head1 Subroutine: render @@ -485,7 +501,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(); @@ -991,11 +1007,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'; @@ -1005,10 +1025,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').') '; @@ -1352,10 +1375,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)) { @@ -1537,7 +1561,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 @@ -1583,41 +1608,45 @@ 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; @@ -1675,6 +1704,23 @@ 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'}; + } while (1) { if ($args->{'sort'}) { @@ -1710,9 +1756,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 @@ -1796,11 +1854,29 @@ 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://})) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + $src .= ($srcHasQuestion? '&' : '?') . 'usehttp=1'; + $srcHasQuestion = 1; + } elsif (($is_ssl) && ($src =~ m{^\Q/adm/wrapper/ext/\E(?!https:)})) { + if ($hostname ne '') { + $src = 'http://'.$hostname.$src; + } + } + } 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).$anchor; + } } # Now, we've decided what parts to show. Loop through them and # show them. @@ -2121,7 +2197,7 @@ sub change_user { - # Now clear the parm cache and reconstruct the parm hash fromt he big_hash + # Now clear the parm cache and reconstruct the parm hash from the big_hash # param.xxxx keys. $self->{PARM_CACHE} = {}; @@ -2214,7 +2290,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; @@ -2586,6 +2662,7 @@ sub parmval { return $self->{PARM_CACHE}->{$hashkey}; } } + my $result = $self->parmval_real($what, $symb, $recurse); $self->{PARM_CACHE}->{$hashkey} = $result; if (wantarray) { @@ -2619,29 +2696,35 @@ sub parmval_real { my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); $mapname = &Apache::lonnet::deversion($mapname); + my ($recursed,@recurseup); + # ----------------------------------------------------- Cascading lookup scheme my $rwhat=$what; $what=~s/^parameter\_//; $what=~s/\_/\./; my $symbparm=$symb.'.'.$what; + my $recurseparm=$mapname.'___(rec).'.$what; my $mapparm=$mapname.'___(all).'.$what; my $usercourseprefix=$cid; - + my $grplevel=$usercourseprefix.'.['.$cgroup.'].'.$what; my $grplevelr=$usercourseprefix.'.['.$cgroup.'].'.$symbparm; + my $grpleveli=$usercourseprefix.'.['.$cgroup.'].'.$recurseparm; my $grplevelm=$usercourseprefix.'.['.$cgroup.'].'.$mapparm; my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what; my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm; + my $secleveli=$usercourseprefix.'.['.$csec.'].'.$recurseparm; my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm; my $courselevel= $usercourseprefix.'.'.$what; my $courselevelr=$usercourseprefix.'.'.$symbparm; + my $courseleveli=$usercourseprefix.'.'.$recurseparm; my $courselevelm=$usercourseprefix.'.'.$mapparm; @@ -2653,6 +2736,23 @@ sub parmval_real { if ($uname and defined($useropt)) { if (defined($$useropt{$courselevelr})) { return [$$useropt{$courselevelr},'resource']; } if (defined($$useropt{$courselevelm})) { return [$$useropt{$courselevelm},'map']; } + if (defined($$useropt{$courseleveli})) { return [$$useropt{$courseleveli},'map']; } + 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},'map']; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what; + if (defined($$useropt{$recursechk})) { return [$$useropt{$recursechk},'map']; } + } if (defined($$useropt{$courselevel})) { return [$$useropt{$courselevel},'course']; } } @@ -2660,12 +2760,46 @@ sub parmval_real { if ($cgroup ne '' and defined($courseopt)) { if (defined($$courseopt{$grplevelr})) { return [$$courseopt{$grplevelr},'resource']; } if (defined($$courseopt{$grplevelm})) { return [$$courseopt{$grplevelm},'map']; } + if (defined($$courseopt{$grpleveli})) { return [$$courseopt{$grpleveli},'map']; } + 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},'map']; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { return [$$courseopt{$recursechk},'map']; } + } if (defined($$courseopt{$grplevel})) { return [$$courseopt{$grplevel},'course']; } } - if ($csec and defined($courseopt)) { + if ($csec ne '' and defined($courseopt)) { if (defined($$courseopt{$seclevelr})) { return [$$courseopt{$seclevelr},'resource']; } if (defined($$courseopt{$seclevelm})) { return [$$courseopt{$seclevelm},'map']; } + if (defined($$courseopt{$secleveli})) { return [$$courseopt{$secleveli},'map']; } + 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},'map']; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { return [$$courseopt{$recursechk},'map']; } + } if (defined($$courseopt{$seclevel})) { return [$$courseopt{$seclevel},'course']; } } @@ -2689,6 +2823,25 @@ sub parmval_real { # --------------------------------------------------- fifth, check more course if (defined($courseopt)) { if (defined($$courseopt{$courselevelm})) { return [$$courseopt{$courselevelm},'map']; } + if (defined($$courseopt{$courseleveli})) { return [$$courseopt{$courseleveli},'map']; } + 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},'map']; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { + return [$$courseopt{$recursechk},'map']; + } + } if (defined($$courseopt{$courselevel})) { my $ret = [$$courseopt{$courselevel},'course']; return $ret; @@ -2711,6 +2864,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' => $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' => $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. @@ -2722,15 +2977,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: @@ -2749,14 +3004,18 @@ 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; @@ -2765,15 +3024,18 @@ sub get_mapparam { # Build the hash keys for the lookup: - my $symbparm=$symb.'.'.$what; my $mapparm=$mapname.'___(all).'.$what; + my $recurseparm=$mapname.'___(rec).'.$what; my $usercourseprefix=$cid; - my $grplevel = "$usercourseprefix.[$cgroup].$mapparm"; - my $seclevel = "$usercourseprefix.[$csec].$mapparm"; - my $courselevel = "$usercourseprefix.$mapparm"; - + my $grplevelm = "$usercourseprefix.[$cgroup].$mapparm"; + my $seclevelm = "$usercourseprefix.[$csec].$mapparm"; + my $courselevelm = "$usercourseprefix.$mapparm"; + + my $grpleveli = "$usercourseprefix.[$cgroup].$recurseparm"; + my $secleveli = "$usercourseprefix.[$csec].$recurseparm"; + my $courseleveli = "$usercourseprefix.$recurseparm"; # Get handy references to the hashes we need in $self: @@ -2786,9 +3048,30 @@ sub get_mapparam { if ($uname and defined($useropt)) { - if (defined($$useropt{$courselevel})) { - return $$useropt{$courselevel}; + if (defined($$useropt{$courselevelm})) { + return $$useropt{$courselevelm}; } + if (defined($$useropt{$courseleveli})) { + return $$useropt{$courseleveli}; + } + 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}; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what; + if (defined($$useropt{$recursechk})) { + return $$useropt{$recursechk}; + } + } } # Check course -- group @@ -2796,38 +3079,100 @@ sub get_mapparam { if ($cgroup ne '' and defined ($courseopt)) { - if (defined($$courseopt{$grplevel})) { - return $$courseopt{$grplevel}; + if (defined($$courseopt{$grplevelm})) { + return $$courseopt{$grplevelm}; } + if (defined($$courseopt{$grpleveli})) { + return $$courseopt{$grpleveli}; + } + 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}; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.['.$cgroup.'].'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { + return $$courseopt{$recursechk}; + } + } } # Check course -- section - - - - if ($csec and defined($courseopt)) { - if (defined($$courseopt{$seclevel})) { - return $$courseopt{$seclevel}; + if ($csec ne '' and defined($courseopt)) { + if (defined($$courseopt{$seclevelm})) { + return $$courseopt{$seclevelm}; } + if (defined($$courseopt{$secleveli})) { + return $$courseopt{$secleveli}; + } + 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}; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.['.$csec.'].'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { + return $$courseopt{$recursechk}; + } + } } # 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; + } } # Additional course parameters: if (defined($courseopt)) { - if (defined($$courseopt{$courselevel})) { - return $$courseopt{$courselevel}; + if (defined($$courseopt{$courselevelm})) { + return $$courseopt{$courselevelm}; } + unless ($recursed) { + @recurseup = $self->recurseup_maps($mapname); + $recursed = 1; + } + if (@recurseup) { + foreach my $item (@recurseup) { + my $norecursechk=$usercourseprefix.'.'.$item.'___(all).'.$what; + if (defined($$courseopt{$norecursechk})) { + if ($what =~ /\.(encrypturl|hiddenresource)$/) { + return $$courseopt{$norecursechk}; + } else { + last; + } + } + my $recursechk=$usercourseprefix.'.'.$item.'___(rec).'.$what; + if (defined($$courseopt{$recursechk})) { + return $$courseopt{$recursechk}; + } + } + } } - return undef; # Unefined if we got here. + return undef; # Undefined if we got here. } sub course_printdates { @@ -2869,10 +3214,6 @@ sub getcourseparam { $what=~s/^parameter\_//; $what=~s/\_/\./; - - my $symbparm = $symb . '.' . $what; - my $mapparm=$mapname.'___(all).'.$what; - # Local refs to the hashes we're going to look at: my $useropt = $self->{USER_OPT}; @@ -3875,7 +4216,7 @@ sub new { # This is a speed optimization, to avoid calling symb() too often. $self->{SYMB} = $self->symb(); - + return $self; } @@ -4001,6 +4342,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)) @@ -4271,6 +4613,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 @@ -4306,6 +4654,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 @@ -4530,11 +4883,12 @@ sub duedate { my $date; my @interval=$self->parmval("interval", $part); my $due_date=$self->parmval("duedate", $part); - if ($interval[0] =~ /\d+/) { - my $first_access=&Apache::lonnet::get_first_access($interval[1], - $self->symb); + if ($interval[0] =~ /^(\d+)/) { + my $timelimit = $1; + my $first_access=&Apache::lonnet::get_first_access($interval[1], + $self->{SYMB}); if (defined($first_access)) { - my $interval = $first_access+$interval[0]; + my $interval = $first_access+$timelimit; $date = (!$due_date || $interval < $due_date) ? $interval : $due_date; } else { @@ -4614,7 +4968,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; } @@ -4634,8 +4988,14 @@ sub getReturnHash { my $self = shift; if (!defined($self->{RETURN_HASH})) { - my %tmpHash = &Apache::lonnet::restore($self->{SYMB},undef,$self->{DOMAIN},$self->{USERNAME}); - $self->{RETURN_HASH} = \%tmpHash; + #my %tmpHash = &Apache::lonnet::restore($self->{SYMB},undef,$self->{DOMAIN},$self->{USERNAME}); + #$self->{RETURN_HASH} = \%tmpHash; + # When info is retrieved for several resources (as when rendering a directory), + # it is much faster to use the user profile dump and avoid repeated lonnet requests + # (especially since lonnet::currentdump is using Lond directly whenever possible, + # and lonnet::restore is not at this point). + $self->{NAV_MAP}->get_user_data(); + $self->{RETURN_HASH} = $self->{NAV_MAP}->{STUDENT_DATA}->{$self->{SYMB}}; } } @@ -4866,7 +5226,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'); @@ -5410,13 +5770,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'}; @@ -5450,19 +5810,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); @@ -5500,6 +5871,17 @@ sub check_for_slot { } } 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'}); } } @@ -5530,6 +5912,17 @@ sub check_for_slot { $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'}); } }