--- loncom/interface/lonnavmaps.pm 2012/02/17 19:25:57 1.479 +++ loncom/interface/lonnavmaps.pm 2021/08/04 19:59:10 1.554 @@ -1,8 +1,7 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.479 2012/02/17 19:25:57 raeburn Exp $ - +# $Id: lonnavmaps.pm,v 1.554 2021/08/04 19:59:10 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -52,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 @@ -65,7 +71,7 @@ processed. Apache::lonnavmaps provides an object model for manipulating this information in a higher-level fashion than directly manipulating -the hash. It also provides access to several auxilary functions +the hash. It also provides access to several auxiliary functions that aren't necessarily stored in the Big Hash, but are a per- resource sort of value, like whether there is any feedback on a given resource. @@ -78,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. @@ -92,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 @@ -164,6 +179,12 @@ If true, the resource's folder will not it. Default is false. True implies printCloseAll is false, since you can't close or open folders when this is on anyhow. +=item * B: + +If true, the title of the folder or page will not be followed by an +icon/link to direct editing of a folder or composite page, originally +added via the Course Editor. + =back =item * B: @@ -225,7 +246,7 @@ automatically. =over 4 -=item * B: default: constructs one from %env A reference to a fresh ::iterator to use from the navmaps. The rendering will reflect the options passed to the iterator, so you can @@ -480,14 +501,15 @@ 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(); +use HTML::Entities; # For debugging -# use Data::Dumper; +#use Data::Dumper; # symbolic constants @@ -570,7 +592,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" @@ -612,49 +638,60 @@ sub getDescription { return &mt("Having technical difficulties; please check status later"); } if ($status == $res->NOTHING_SET) { - return &Apache::lonhtmlcommon::direct_parm_link(&mt("Not currently assigned.",$res->symb(),'opendate'),$part); + return &Apache::lonhtmlcommon::direct_parm_link(&mt('Not currently assigned'),$res->symb(),'opendate',$part); } 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) { @@ -669,7 +706,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"); @@ -691,7 +738,7 @@ sub getDescription { my $maxtries = $res->maxtries($part); my $triesString = ""; if ($tries && $maxtries) { - $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; if ($maxtries > 1 && $maxtries - $tries == 1) { $triesString = "$triesString"; } @@ -900,6 +947,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... @@ -917,11 +967,32 @@ sub render_resource { $newBranchText = ".mt('Branch')."; } - # links to open and close the folder - my $whitespace = $location.'/whitespace_21.gif'; - my $linkopen = "".""; - my $linkclose = ""; + my ($nomodal,$linkopen,$linkclose); + unless ($resource->is_map() || $params->{'resource_nolink'}) { + $linkopen = ""; + $linkclose = ""; + 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 .= ""; + } + } # Default icon: unknown page my $icon = ""; @@ -969,33 +1040,63 @@ sub render_resource { '&jump=' . &escape($resource->symb()) . "&folderManip=1\">"; - + $linkclose = ''; } else { # Don't allow users to manipulate folder $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; $icon = ""."\"".($nowOpen"; - - $linkopen = ""; - $linkclose = ""; + if ($params->{'caller'} eq 'sequence') { + $linkopen = ""; + $linkclose = ''; + } 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/)) { - my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png'; - $editmapLink=' '. - ''. + if (!$params->{'map_no_edit_link'}) { + my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png'; + $editmapLink=' '. + ''. ''.&mt('Edit Content').''. ''; + } + } + if ($params->{'mapHidden'} || $resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($params->{'mapUnlisted'}) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif ($params->{'mapHiddenDeepLink'} || $resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; + } + } else { + if ($resource->randomout()) { + $nonLinkedText .= ' ('.&mt('hidden').') '; + } elsif ($resource->deeplinkout()) { + $nonLinkedText .= ' ('.&mt('not shown').') '; + } else { + my $deeplink = $resource->deeplink($params->{caller}); + if ((($deeplink eq 'absent') || ($deeplink eq 'grades')) && + &advancedUser()) { + $nonLinkedText .= ' ('.&mt('unlisted').') '; + } elsif (($deeplink) && ($deeplink) ne 'full') { + if (&advancedUser()) { + $nonLinkedText .= ' ('.&mt('deep-link access'). + ') '; + } else { + $nonLinkedText .= ' ('.&mt('access via external site'). + ') '; + } + } } - } - - if ($resource->randomout()) { - $nonLinkedText .= ' ('.&mt('hidden').') '; } if (!$resource->condval()) { $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; } if (($resource->is_practice()) && ($resource->is_raw_problem())) { - $nonLinkedText .=' '.&mt('not graded').''; + $nonLinkedText .=' '.&mt('not graded').''; } # We're done preparing and finally ready to start the rendering @@ -1039,10 +1140,19 @@ sub render_resource { } if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { - $result .= "$curMarkerBegin$title$partLabel$curMarkerEnd$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; } @@ -1104,7 +1214,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}; @@ -1128,21 +1238,28 @@ sub render_long_status { $params->{'multipart'} && $part eq "0"; my $color; - if ($resource->is_problem() || $resource->is_practice()) { + my $info = ''; + if ($resource->is_gradable() || $resource->is_practice()) { $color = $colormap{$resource->status}; - - if (dueInLessThan24Hours($resource, $part) || - lastTry($resource, $part)) { + + if (dueInLessThan24Hours($resource, $part)) { $color = $hurryUpColor; - } + $info = ' title="'.&mt('Due in less than 24 hours!').'"'; + } elsif (lastTry($resource, $part)) { + unless (($resource->problemstatus($part) eq 'no') || + ($resource->problemstatus($part) eq 'no_feedback_ever')) { + $color = $hurryUpColor; + $info = ' title="'.&mt('One try remaining!').'"'; + } + } } - - 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 .= ""; } + if ($color) {$result .= ''; } $result .= getDescription($resource, $part); - if ($color) {$result .= ""; } + if ($color) {$result .= ""; } } if ($resource->is_map() && &advancedUser() && $resource->randompick()) { $result .= &mt('(randomly select [_1])', $resource->randompick()); @@ -1185,7 +1302,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 ''; } @@ -1224,11 +1341,11 @@ sub render_parts_summary_status { } $return.= $td . $totalParts . ' parts: '; foreach my $status (@statuses) { - if ($overallstatus{$status}) { - $return.="" . $overallstatus{$status} . ' ' - . $statusStrings{$status} . ""; - } + if ($overallstatus{$status}) { + $return.='' . $overallstatus{$status} . ' ' + . $statusStrings{$status} . ''; + } } $return.= $endtd; return $return; @@ -1296,7 +1413,10 @@ sub render { # Without renaming the filterfunc, the server seems to go into # an infinite loop my $oldFilterFunc = $filterFunc; - $filterFunc = sub { my $res = shift; return !$res->randomout() && + $filterFunc = sub { my $res = shift; return !$res->randomout() && + ($res->deeplink($args->{'caller'}) ne 'absent') && + ($res->deeplink($args->{'caller'}) ne 'grades') && + !$res->deeplinkout() && &$oldFilterFunc($res);}; } @@ -1326,10 +1446,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)) { @@ -1389,16 +1510,18 @@ 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'}); } } - # (re-)Locate the jump point, if any # Note this does not take filtering or hidden into account... need # to be fixed? @@ -1482,9 +1605,10 @@ sub render { 'document.clearbubbles.submit()', 'Mark all posts read'); my $time=time; + my $querystr = &HTML::Entities::encode($ENV{'QUERY_STRING'},'<>&"'); $result .= (< - + END if ($env{'form.register'}) { @@ -1511,7 +1635,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 @@ -1557,41 +1682,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; @@ -1649,13 +1778,34 @@ 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'}) { $curRes = shift(@resources); } else { $curRes = $it->next($closeAllPages); - } if (!$curRes) { last; } @@ -1685,9 +1835,39 @@ 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; + $args->{'mapUnlisted'} = 0; + $args->{'mapHiddenDeepLink'} = 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; + } + } elsif ($curRes->deeplinkout) { + if ($userCanSeeHidden) { + $args->{'mapHiddenDeepLink'} = 1; + } else { + next; + } + } else { + my $deeplink = $navmap->get_mapparam(undef,$mapname,"0.deeplink"); + my ($state,$others,$listed) = split(/,/,$deeplink); + if (($listed eq 'absent') || ($listed eq 'grades')) { + if ($userCanSeeHidden) { + $args->{'mapUnlisted'} = 1; + } else { + next; + } + } + } + } } # If we're suppressing navmaps and this is a navmap, continue on @@ -1748,8 +1928,17 @@ END $args->{'condensed'} = 1; } } - } - + } + # If deep-link parameter is set (and is not set to full) suppress link + # unless privileged user, tinyurl used for login resolved to a map, and + # the resource is within the map. + if ((!$curRes->deeplink($args->{'caller'})) || + ($curRes->deeplink($args->{'caller'}) eq 'full') || &advancedUser()) { + $args->{'resource_nolink'} = 0; + } else { + $args->{'resource_nolink'} = 1; + } + # If the multipart problem was condensed, "forget" it was multipart if (scalar(@parts) == 1) { $args->{'multipart'} = 0; @@ -1771,11 +1960,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. @@ -1803,7 +2016,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"; @@ -1827,6 +2040,8 @@ END } } } + + $result.=&Apache::loncommon::end_data_table(); # Print out the part that jumps to #curloc if it exists # delay needed because the browser is processing the jump before @@ -1843,8 +2058,6 @@ if (location.href.indexOf('#curloc')==-1 "); } - $result.=&Apache::loncommon::end_data_table(); - if ($r) { $r->print($result); $result = ""; @@ -1875,7 +2088,6 @@ sub show_linkitems_toolbar { ''.&mt('Tools:').''; } $result .= ''."\n". - ''."\n". '
    '; my @linkorder = ('firsthomework','everything','uncompleted', 'changefolder','clearbubbles','edittoplevel'); @@ -1903,7 +2115,7 @@ sub show_linkitems_toolbar { } } $result .= '
'. - '
'; + ''; if (($numlinks==1) && (exists($args->{'linkitems'}{'edittoplevel'}))) { $result .= '
'. &mt('Content Editor').''; @@ -2006,10 +2218,12 @@ sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; - bless($self); # So we can call change_user if neceesary + bless($self); # So we can call change_user if necessary $self->{USERNAME} = shift || $env{'user.name'}; $self->{DOMAIN} = shift || $env{'user.domain'}; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; @@ -2023,10 +2237,14 @@ sub new { # We can only tie the nav hash as done below if the username/domain # match the env one. Otherwise change_user does everything we need...since we can't - # assume there are course hashes for the specific requested user@domamin: + # assume there are course hashes for the specific requested user:domain + # Note: change_user is also called if we need the nav hash when printing CODEd + # assignments or printing an exam, in which the enclosing folder for the items in + # the exam has hidden set. # - if ( ($self->{USERNAME} eq $env{'user.name'}) && ($self->{DOMAIN} eq $env{'user.domain'})) { + if (($self->{USERNAME} eq $env{'user.name'}) && ($self->{DOMAIN} eq $env{'user.domain'}) && + !$self->{CODE} && !$self->{NOHIDE}) { # tie the nav hash @@ -2049,11 +2267,9 @@ sub new { $self->{PARM_HASH} = \%parmhash; $self->{PARM_CACHE} = {}; } else { - $self->change_user($self->{USERNAME}, $self->{DOMAIN}); + $self->change_user($self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}); } - my $d = Data::Dumper->new([$self]); - return $self; } @@ -2064,12 +2280,15 @@ sub new { # Parameters: # user - New user. # domain- Domain the user belongs to. +# code - Anonymous CODE in use. # Implicit inputs: # sub change_user { my $self = shift; $self->{USERNAME} = shift; $self->{DOMAIN} = shift; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; # If the hashes are already tied make sure to break that bond: @@ -2085,18 +2304,18 @@ sub change_user { my ($cdom, $cnum) = split(/\_/, $env{'request.course.id'}); my %big_hash; - &Apache::lonmap::loadmap($cnum, $cdom, $self->{USERNAME}, $self->{DOMAIN}, \%big_hash); + &Apache::lonmap::loadmap($cnum, $cdom, $self->{USERNAME}, $self->{DOMAIN}, $self->{CODE}, $self->{NOHIDE}, \%big_hash); $self->{NAV_HASH} = \%big_hash; - # 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} = {}; my %parm_hash = {}; - foreach my $key (keys %big_hash) { + foreach my $key (keys(%big_hash)) { if ($key =~ /^param\./) { my $param_key = $key; $param_key =~ s/^param\.//; @@ -2105,11 +2324,8 @@ sub change_user { } $self->{PARM_HASH} = \%parm_hash; - - - -} +} sub generate_course_user_opt { my $self = shift; @@ -2151,6 +2367,8 @@ sub generate_course_user_opt { return; } + + sub generate_email_discuss_status { my $self = shift; my $symb = shift; @@ -2170,7 +2388,7 @@ sub generate_email_discuss_status { my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss', $self->{DOMAIN},$self->{USERNAME},'lastread'); my %lastreadtime = (); - foreach my $key (keys %lastread) { + foreach my $key (keys(%lastread)) { my $shortkey = $key; $shortkey =~ s/_lastread$//; $lastreadtime{$shortkey} = $lastread{$key}; @@ -2184,7 +2402,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; @@ -2285,7 +2503,7 @@ sub getIterator { my $self = shift; my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, shift, undef, shift, - shift, shift); + shift, shift, shift); return $iterator; } @@ -2556,6 +2774,7 @@ sub parmval { return $self->{PARM_CACHE}->{$hashkey}; } } + my $result = $self->parmval_real($what, $symb, $recurse); $self->{PARM_CACHE}->{$hashkey} = $result; if (wantarray) { @@ -2564,10 +2783,12 @@ sub parmval { return $result->[0]; } + sub parmval_real { my $self = shift; my ($what,$symb,$recurse) = @_; + # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated $self->generate_course_user_opt(); @@ -2587,27 +2808,42 @@ 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; + } + 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; + my $useropt = $self->{USER_OPT}; my $courseopt = $self->{COURSE_OPT}; my $parmhash = $self->{PARM_HASH}; @@ -2616,6 +2852,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']; } } @@ -2623,12 +2876,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']; } } @@ -2645,13 +2932,32 @@ 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)) { 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; @@ -2670,11 +2976,413 @@ 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 @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. +# +sub map_printdates { + my ($self, $res, $part) = @_; + + + + + + 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, $mapname, $what) = @_; + + # Ensure the course option hash is populated: + + $self->generate_course_user_opt(); + + # Get the course id and section if there is one. + + my $cid=$env{'request.course.id'}; + my $csec=$env{'request.course.sec'}; + my $cgroup=''; + my @cgrps=split(/:/,$env{'request.course.groups'}); + if (@cgrps > 0) { + @cgrps = sort(@cgrps); + $cgroup = $cgrps[0]; + } + my $uname=$self->{USERNAME}; + my $udom=$self->{DOMAIN}; + + unless ($symb || $mapname) { return; } + my $result=''; + my ($recursed,@recurseup); + + + # Figure out which map we are in. + + 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\_//; + $what=~s/\_/\./; + + # Build the hash keys for the lookup: + + my $mapparm=$mapname.'___(all).'.$what; + my $recurseparm=$mapname.'___(rec).'.$what; + my $usercourseprefix=$cid; + + + 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: + + my $useropt = $self->{USER_OPT}; + my $courseopt = $self->{COURSE_OPT}; + my $parmhash = $self->{PARM_HASH}; + + # Check per user + + + + if ($uname and defined($useropt)) { + 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 + + + + if ($cgroup ne '' and defined ($courseopt)) { + 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 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: + + if ($symb) { + my $symbparm=$symb.'.'.$what; + my $thisparm = $$parmhash{$symbparm}; + if (defined($thisparm)) { + return $thisparm; + } + } + + + # Additional course parameters: + + if (defined($courseopt)) { + if (defined($$courseopt{$courselevelm})) { + return $$courseopt{$courselevelm}; + } + if (defined($$courseopt{$courseleveli})) { + return $$courseopt{$courseleveli}; + } + 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; # Undefined if we got here. +} + +sub course_printdates { + my ($self, $symb, $part) = @_; + + + my $opendate = $self->getcourseparam($symb, $part . '.printstartdate'); + my $closedate = $self->getcourseparam($symb, $part . '.printenddate'); + return ($opendate, $closedate); + +} + +sub getcourseparam { + my ($self, $symb, $what) = @_; + + $self->generate_course_user_opt(); # If necessary populate the hashes. + + my $uname = $self->{USERNAME}; + my $udom = $self->{DOMAIN}; + + # Course, section, 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'}); + if(@cgroups > 0) { + @cgroups = sort(@cgroups); + $cgroup = $cgroups[0]; # There is a course group. + } + my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); + $mapname = &Apache::lonnet::deversion($mapname); + + # + # Make the various lookup keys: + # + + $what=~s/^parameter\_//; + $what=~s/\_/\./; + + # Local refs to the hashes we're going to look at: + + my $useropt = $self->{USER_OPT}; + my $courseopt = $self->{COURSE_OPT}; + + # + # We want the course level stuff from the way + # parmval_real operates + # TODO: Factor some of this stuff out of + # both parmval_real and here + # + my $courselevel = $cid . '.' . $what; + my $grplevel = $cid . '.[' . $cgroup . ']' . $what; + my $seclevel = $cid . '.[' . $csec . ']' . $what; + + + # Try for the user's course level option: + + if ($uname and defined($useropt)) { + if (defined($$useropt{$courselevel})) { + return $$useropt{$courselevel}; + } + } + # Try for the group's course level option: + + if ($cgroup ne '' and defined($courseopt)) { + if (defined($$courseopt{$grplevel})) { + return $$courseopt{$grplevel}; + } + } + + # Try for section level parameters: + + if ($csec ne '' and defined($courseopt)) { + if (defined($$courseopt{$seclevel})) { + return $$courseopt{$seclevel}; + } + } + # Try for 'additional' course parameters: + + if (defined($courseopt)) { + if (defined($$courseopt{$courselevel})) { + return $$courseopt{$courselevel}; + } + } + return undef; + +} + + =pod =item * B(url,multiple): @@ -2689,7 +3397,7 @@ resource appears multiple times in the c will be returned (useful for maps), unless the multiple parameter has been included, in which case all instances are returned in an array. -=item * B(map, filterFunc, recursive, bailout, showall): +=item * B(map, filterFunc, recursive, bailout, showall, noblockcheck): The map is a specification of a map to retreive the resources from, either as a url or as an object. The filterFunc is a reference to a @@ -2698,13 +3406,18 @@ true if the resource should be included, be. If recursive is true, the map will be recursively examined, otherwise it will not be. If bailout is true, the function will return as soon as it finds a resource, if false it will finish. If showall is -true it will not hide maps that contain nothing but one other map. By -default, the map is the top-level map of the course, filterFunc is a -function that always returns 1, recursive is true, bailout is false, -showall is false. The resources will be returned in a list containing -the resource objects for the corresponding resources, with B in the list; regardless of branching, -recursion, etc., it will be a flat list. +true it will not hide maps that contain nothing but one other map. The +noblockcheck arg is propagated to become the sixth arg in the call to +lonnet::allowed when checking a resource's availability during collection +of resources using the iterator. noblockcheck needs to be true if +retrieveResources() was called by a routine that itself was called by +lonnet::allowed, in order to avoid recursion. By default the map +is the top-level map of the course, filterFunc is a function that +always returns 1, recursive is true, bailout is false, showall is +false. The resources will be returned in a list containing the +resource objects for the corresponding resources, with B in the list; regardless of branching, recursion, etc., +it will be a flat list. Thus, this is suitable for cases where you don't want the structure, just a list of all resources. It is also suitable for finding out how @@ -2771,6 +3484,7 @@ sub retrieveResources { my $bailout = shift; if (!defined($bailout)) { $bailout = 0; } my $showall = shift; + my $noblockcheck = shift; # Create the necessary iterator. if (!ref($map)) { # assume it's a url of a map. $map = $self->getResourceByUrl($map); @@ -2800,7 +3514,7 @@ sub retrieveResources { # Run down the iterator and collect the resources. my $curRes; - while ($curRes = $it->next()) { + while ($curRes = $it->next(undef,$noblockcheck)) { if (ref($curRes)) { if (!&$filterFunc($curRes)) { next; @@ -2834,6 +3548,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; @@ -2864,7 +3643,7 @@ getIterator behaves as follows: =over 4 -=item * B(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap): +=item * B(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap, $deeplinklisted): All parameters are optional. firstResource is a resource reference corresponding to where the iterator should start. It defaults to @@ -2881,7 +3660,10 @@ that is not just a single, 'redirecting' will return all information, starting with the top-level map, regardless of content. returnTopMap, if true (default false), will cause the iterator to return the top-level map object (resource 0.0) -before anything else. +before anything else. deeplinklisted if true (default false), will +check "listed" status of a resource with a deeplink, and unless "absent" +will exclude deeplink checking when retrieving the browsePriv from +lonnet::allowed(). Thus, by default, only top-level resources will be shown. Change the condition to a 1 without changing the hash, and all resources will be @@ -2951,7 +3733,14 @@ Note that inside of the loop, it's frequ resource objects will be references, and any non-references will be the tokens described above. -Also note there is some old code floating around that trys to track +The next() routine can take two (optional) arguments: +closeAllPages - if true will not recurse down a .page +noblockcheck - passed to browsePriv() for passing as sixth arg to +call to lonnet::allowed. This needs to be set if retrieveResources +was already called from another routine called within lonnet::allowed, +so as to prevent recursion. + +Also note there is some old code floating around that tries to track the depth of the iterator to see when it's done; do not copy that code. It is difficult to get right and harder to understand than this. They should be migrated to this new style. @@ -3011,6 +3800,10 @@ sub new { # have we done that yet? $self->{HAVE_RETURNED_0} = 0; + # Do we want to check the "listed" status for a resource for which + # deeplinking applies. + $self->{DEEPLINKLISTED} = shift; + # Now, we need to pre-process the map, by walking forward and backward # over the parts of the map we're going to look at. @@ -3098,16 +3891,12 @@ sub new { if ($resourceCount == 1 && $resource->is_sequence() && !$self->{FORCE_TOP}) { my $firstResource = $resource->map_start(); my $finishResource = $resource->map_finish(); - my $result; - my $rdump = Data::Dumper->new([$result]); - $result = Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, - $finishResource, $self->{FILTER}, - $self->{ALREADY_SEEN}, - $self->{CONDITION}, - $self->{FORCE_TOP}); - return $result; - - + return Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, + $finishResource, $self->{FILTER}, + $self->{ALREADY_SEEN}, + $self->{CONDITION}, + $self->{FORCE_TOP}, + undef,$self->{DEEPLINKLISTED}); } # Set up some bookkeeping information. @@ -3126,14 +3915,13 @@ sub new { $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; bless ($self); - my $selfDump = Data::Dumper->new([$self]); - return $self; } sub next { my $self = shift; my $closeAllPages=shift; + my $noblockcheck = shift; if ($self->{FINISHED}) { return END_ITERATOR(); } @@ -3143,7 +3931,6 @@ sub next { if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) { $self->{HAVE_RETURNED_0} = 1; my $nextTopLevel = $self->{NAV_MAP}->getById('0.0'); - return $self->{NAV_MAP}->getById('0.0'); } if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0_BEGIN_MAP}) { @@ -3169,7 +3956,6 @@ sub next { if (defined($self->{FORCE_NEXT})) { my $tmp = $self->{FORCE_NEXT}; $self->{FORCE_NEXT} = undef; - return $tmp; } @@ -3270,7 +4056,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(); @@ -3279,20 +4066,19 @@ sub next { $finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, $self->{CONDITION}, - $self->{FORCE_TOP}); + $self->{FORCE_TOP}, + undef,$self->{DEEPLINKLISTED}); } # If this is a blank resource, don't actually return it. # Should you ever find you need it, make sure to add an option to the code # that you can use; other things depend on this behavior. - my $browsePriv = $self->{HERE}->browsePriv(); + my $browsePriv = $self->{HERE}->browsePriv($noblockcheck,$self->{DEEPLINKLISTED}); if (!$self->{HERE}->src() || (!($browsePriv eq 'F') && !($browsePriv eq '2')) ) { return $self->next($closeAllPages); } - my $hereResource = $self->{HERE}; - return $self->{HERE}; } @@ -3459,7 +4245,6 @@ sub next { my $nextUnfiltered; if ($self->{DIRECTION} == FORWARD()) { $nextUnfiltered = $self->{HERE}->getNext(); - } else { $nextUnfiltered = $self->{HERE}->getPrevious(); } @@ -3620,9 +4405,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; } @@ -3713,6 +4501,7 @@ sub from { my $self=shift; return $self- sub goesto { my $self=shift; return $self->navHash("goesto_", 1); } sub kind { my $self=shift; return $self->navHash("kind_", 1); } sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } +sub deeplinkout { my $self=shift; return $self->navHash("deeplinkout_", 1); } sub randompick { my $self = shift; my $randompick = $self->parmval('randompick'); @@ -3734,8 +4523,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; @@ -3748,6 +4537,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)) @@ -3756,7 +4546,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; @@ -3880,6 +4670,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' # @@ -3967,7 +4770,6 @@ sub is_task { sub is_empty_sequence { my $self=shift; - my $src = $self->src(); return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc()); } @@ -3979,7 +4781,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 @@ -4012,12 +4814,18 @@ resource of the map. Returns a string with the type of the map in it. -=item *B: +=item * B: Returns a string with a comma-separated ordered list of map_pc IDs 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 @@ -4053,6 +4861,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 @@ -4083,11 +4896,13 @@ their code.) =over 4 + =item * B returns true if the current date is such that the specified resource part is printable. + =item * B Returns true if all parts in the resource are printable making the @@ -4146,23 +4961,38 @@ Get the weight for the problem. =cut -sub printable { - my ($self, $part) = @_; - # Get the print open/close dates for the resource. - my $start = $self->parmval("prinstartdate", $part); - my $end = $self->parmval("printenddate", $part); +sub printable { + + my ($self, $part) = @_; # The following cases apply: - # - No dates set: Printable. + # - If a start date is not set, it is replaced by the open date. + # - Ditto for start/open replaced by content open. + # - If neither start nor printdates are set the part is printable. # - Start date set but no end date: Printable if now >= start date. # - End date set but no start date: Printable if now <= end date. # - both defined: printable if start <= now <= end # + + # Get the print open/close dates for the resource. + + my $start = $self->parmval("printstartdate", $part); + my $end = $self->parmval("printenddate", $part); + + if (!$start) { + $start = $self->parmval("opendate", $part); + } + if (!$start) { + $start = $self->parmval("contentopen", $part); + } + + my $now = time(); + my $startok = 1; my $endok = 1; @@ -4183,7 +5013,7 @@ sub resprintable { my $partsref = $self->parts(); my @parts = @$partsref; - if ((!defined(@parts)) || (scalar(@parts) == 0)) { + if (!@parts) { return $self->printable(0); } else { foreach my $part (@parts) { @@ -4215,25 +5045,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; @@ -4241,9 +5071,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 @@ -4260,11 +5090,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 { @@ -4335,7 +5166,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; @@ -4344,7 +5175,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; } @@ -4358,14 +5189,60 @@ sub slot_control { my $available = $self->parmval("available", $part); return ($useslots,$availablestudent,$available); } +sub deeplink { + my ($self,$caller,$action) = @_; + my $deeplink = $self->parmval("deeplink"); + if ($deeplink) { + my ($state,$others,$listed,$scope) = split(/,/,$deeplink); + if ($action eq 'getlisted') { + return $listed; + } + if ($env{'request.deeplink.login'}) { + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $deeplink_symb = &Apache::loncommon::deeplink_login_symb($cnum,$cdom); + if ($deeplink_symb) { + my ($loginmap,$mapname); + if ($deeplink_symb =~ /\.(page|sequence)$/) { + $mapname = $self->enclosing_map_src(); + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[2]); + return if ($mapname eq $loginmap); + } else { + return if ($deeplink_symb eq $self->symb()); + if (($scope eq 'map') || ($scope eq 'rec')) { + $mapname = $self->enclosing_map_src(); + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[0]); + return if ($mapname eq $loginmap); + } + } + if ($scope eq 'rec') { + my $map_pc = $self->navHash('map_pc_'.$mapname); + my @recurseup = split(/,/,$self->navHash('map_hierarchy_'.$map_pc)); + my $login_pc = $self->navHash('map_pc_'.$loginmap); + return if (grep(/^\Q$login_pc\E$/,@recurseup)); + } + } + } + unless (($caller eq 'sequence') || ($state eq 'both')) { + return $listed; + } + } + return; +} # Multiple things need this 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}}; } } @@ -4429,23 +5306,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); } @@ -4453,7 +5330,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); } @@ -4506,6 +5383,8 @@ sub parts { my $self = shift; if ($self->ext) { return []; } + if (($self->is_tool()) && + ($self->is_gradable())) { return ['0']; } $self->extractParts(); return $self->{PARTS}; @@ -4596,14 +5475,14 @@ 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'); 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; } @@ -4621,17 +5500,17 @@ 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; } } } - my @sortedParts = sort keys %parts; + my @sortedParts = sort(keys(%parts)); $self->{PARTS} = \@sortedParts; } @@ -4652,13 +5531,13 @@ sub extractParts { # So we have to use our knowlege of part names to figure out # where the part names begin and end, and even then, it is possible # to construct ambiguous situations. - foreach my $data (split /,/, $metadata) { + foreach my $data (split(/,/, $metadata)) { if ($data =~ /^([a-zA-Z]+)response_(.*)/ || $data =~ /^(Task)_(.*)/) { my $responseType = $1; my $partStuff = $2; my $partIdSoFar = ''; - my @partChunks = split /_/, $partStuff; + my @partChunks = split(/_/, $partStuff); my $i = 0; for ($i = 0; $i < scalar(@partChunks); $i++) { if ($partIdSoFar) { $partIdSoFar .= '_'; } @@ -4880,6 +5759,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 @@ -4892,6 +5779,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; @@ -4906,8 +5795,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') { @@ -5061,7 +5954,8 @@ sub status { # There are a few whole rows we can dispose of: if ($completionStatus == CORRECT || - $completionStatus == CORRECT_BY_OVERRIDE ) { + $completionStatus == CORRECT_BY_OVERRIDE || + $completionStatus == CORRECT_BY_PASSBACK ) { if ( $suppressFeedback ) { return ANSWER_SUBMITTED } my $awarded=$self->awarded($part); if ($awarded < 1 && $awarded > 0) { @@ -5074,7 +5968,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; } @@ -5116,7 +6011,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; @@ -5131,7 +6027,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)); @@ -5140,13 +6036,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'}; @@ -5159,7 +6055,7 @@ sub check_for_slot { } else { if ($ip ne '') { if (!&Apache::loncommon::check_ip_acc($ip)) { - return (RESERVED_LOCATION,$ip,$slot_name); + return (RESERVED_LOCATION,$end,$slot_name); } } my @proctors; @@ -5170,7 +6066,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); @@ -5180,19 +6076,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); @@ -5205,18 +6112,83 @@ sub check_for_slot { my $reservable = &Apache::lonnet::get_reservable_slots($cnum,$cdom,$env{'user.name'}, $env{'user.domain'}); if (ref($reservable) eq 'HASH') { + my ($map) = &Apache::lonnet::decode_symb($symb); if ((ref($reservable->{'now_order'}) eq 'ARRAY') && (ref($reservable->{'now'}) eq 'HASH')) { foreach my $slot (reverse (@{$reservable->{'now_order'}})) { - if (($reservable->{'now'}{$slot}{'symb'} eq '') || - ($reservable->{'now'}{$slot}{'symb'} eq $symb)) { + my $canuse; + if ($reservable->{'now'}{$slot}{'symb'} eq '') { + $canuse = 1; + } else { + my %oksymbs; + my @slotsymbs = split(/\s*,\s*/,$reservable->{'now'}{$slot}{'symb'}); + map { $oksymbs{$_} = 1; } @slotsymbs; + if ($oksymbs{$symb}) { + $canuse = 1; + } else { + foreach my $item (@slotsymbs) { + if ($item =~ /\.(page|sequence)$/) { + (undef,undef, my $sloturl) = &Apache::lonnet::decode_symb($item); + if (($map ne '') && ($map eq $sloturl)) { + $canuse = 1; + last; + } + } + } + } + } + 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'}}) { - if (($reservable->{'future'}{$slot}{'symb'} eq '') || - ($reservable->{'future'}{$slot}{'symb'} eq $symb)) { + my $canuse; + if ($reservable->{'future'}{$slot}{'symb'} eq '') { + $canuse = 1; + } elsif ($reservable->{'future'}{$slot}{'symb'} =~ /,/) { + my %oksymbs; + my @slotsymbs = split(/\s*,\s*/,$reservable->{'future'}{$slot}{'symb'}); + map { $oksymbs{$_} = 1; } @slotsymbs; + if ($oksymbs{$symb}) { + $canuse = 1; + } else { + foreach my $item (@slotsymbs) { + if ($item =~ /\.(page|sequence)$/) { + (undef,undef, my $sloturl) = &Apache::lonnet::decode_symb($item); + if (($map ne '') && ($map eq $sloturl)) { + $canuse = 1; + last; + } + } + } + } + } elsif ($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'}); } } @@ -5367,6 +6339,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 @@ -5402,7 +6407,7 @@ sub getPrevious { my $self = shift; my @branches; my $from = $self->from(); - foreach my $branch ( split /,/, $from) { + foreach my $branch ( split(/,/, $from)) { my $choice = $self->{NAV_MAP}->getById($branch); my $prev = $choice->comesfrom(); $prev = $self->{NAV_MAP}->getById($prev); @@ -5414,12 +6419,24 @@ sub getPrevious { sub browsePriv { my $self = shift; + my $noblockcheck = shift; + my $deeplinklisted = shift; if (defined($self->{BROWSE_PRIV})) { return $self->{BROWSE_PRIV}; } - + my ($nodeeplinkcheck,$nodeeplinkout); + if ($deeplinklisted) { + my $deeplink = $self->deeplink(undef,'getlisted'); + if (($deeplink) && ($deeplink ne 'absent')) { + $nodeeplinkcheck = 1; + } + $nodeeplinkout = 1; + } $self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre',$self->src(), - $self->symb()); + $self->{SYMB},undef, + undef,$noblockcheck, + undef,$nodeeplinkcheck, + $nodeeplinkout); } =pod