# The LearningOnline Network with CAPA
# Navigate Maps Handler
#
# $Id: lonnavmaps.pm,v 1.116 2002/11/26 16:25:36 bowersj2 Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
# (Page Handler
#
# (TeX Content Handler
#
# 05/29/00,05/30 Gerd Kortemeyer)
# 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23,
# 10/02,10/10,10/14,10/16,10/18,10/19,10/31,11/6,11/14,11/16 Gerd Kortemeyer)
#
# 3/1/1,6/1,17/1,29/1,30/1,2/8,9/21,9/24,9/25 Gerd Kortemeyer
# YEAR=2002
# 1/1 Gerd Kortemeyer
# Oct-Nov Jeremy Bowers
package Apache::lonnavmaps;
use strict;
use Apache::Constants qw(:common :http);
use Apache::loncommon();
use POSIX qw (floor strftime);
sub handler {
my $r = shift;
&Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING});
# Handle header-only request
if ($r->header_only) {
if ($ENV{'browser.mathml'}) {
$r->content_type('text/xml');
} else {
$r->content_type('text/html');
}
$r->send_http_header;
return OK;
}
# Send header, don't cache this page
if ($ENV{'browser.mathml'}) {
$r->content_type('text/xml');
} else {
$r->content_type('text/html');
}
&Apache::loncommon::no_cache($r);
$r->send_http_header;
# Create the nav map
my $navmap = Apache::lonnavmaps::navmap->new(
$ENV{"request.course.fn"}.".db",
$ENV{"request.course.fn"}."_parms.db", 1, 1);
if (!defined($navmap)) {
my $requrl = $r->uri;
$ENV{'user.error.msg'} = "$requrl:bre:0:0:Course not initialized";
return HTTP_NOT_ACCEPTABLE;
}
$r->print("
\n");
$r->print("Navigate Course Contents");
# Header
$r->print(&Apache::loncommon::bodytag('Navigate Course Contents','',
''));
$r->print('');
$r->print('');
my $condition = 0;
if ($ENV{'form.condition'}) {
$condition = 1;
}
my $currenturl = $ENV{'form.postdata'};
$currenturl=~s/^http\:\/\///;
$currenturl=~s/^[^\/]+//;
# alreadyHere allows us to only open the maps necessary to view
# the current location once, while at the same time remembering
# the current location. Without that check, the user would never
# be able to close those maps; the user would close it, and the
# currenturl scan would re-open it.
my $queryAdd = "postdata=" . &Apache::lonnet::escape($currenturl) .
"&alreadyHere=1";
if ($condition) {
$r->print("Close All Folders");
} else {
$r->print("Open All Folders");
}
$r->print('
');
$r->rflush();
# Now that we've displayed some stuff to the user, init the navmap
$navmap->init();
# Check that it's defined
if (!($navmap->courseMapDefined())) {
$r->print('Coursemap undefined.' .
'');
return OK;
}
# Grab a resource object so we have access to the constants; this
# is technically not proper, but should be harmless
my $res = $navmap->firstResource();
# These are some data tables, which make it easy to change some of
# of the specific visualization parameters if desired.
# Defines a status->color mapping, null string means don't color
my %colormap =
( $res->NETWORK_FAILURE => '',
$res->CORRECT => '',
$res->EXCUSED => '#3333FF',
$res->PAST_DUE_ANSWER_LATER => '',
$res->PAST_DUE_NO_ANSWER => '',
$res->ANSWER_OPEN => '#006600',
$res->OPEN_LATER => '',
$res->TRIES_LEFT => '',
$res->INCORRECT => '',
$res->OPEN => '',
$res->NOTHING_SET => '' );
# And a special case in the nav map; what to do when the assignment
# is not yet done and due in less then 24 hours
my $hurryUpColor = "#FF0000";
# Keep these mappings in sync with lonquickgrades, which uses the colors
# instead of the icons.
my %statusIconMap =
( $res->NETWORK_FAILURE => '',
$res->NOTHING_SET => '',
$res->CORRECT => 'navmap.correct.gif',
$res->EXCUSED => 'navmap.correct.gif',
$res->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif',
$res->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif',
$res->ANSWER_OPEN => 'navmap.wrong.gif',
$res->OPEN_LATER => '',
$res->TRIES_LEFT => 'navmap.open.gif',
$res->INCORRECT => 'navmap.wrong.gif',
$res->OPEN => 'navmap.open.gif',
$res->ATTEMPTED => 'navmap.open.gif' );
my %iconAltTags =
( 'navmap.correct.gif' => 'Correct',
'navmap.wrong.gif' => 'Incorrect',
'navmap.open.gif' => 'Open' );
my %condenseStatuses =
( $res->NETWORK_FAILURE => 1,
$res->NOTHING_SET => 1,
$res->CORRECT => 1 );
my %filterHash;
# Figure out what we're not displaying
foreach (split(/\,/, $ENV{"form.filter"})) {
if ($_) {
$filterHash{$_} = "1";
}
}
# Is this a new-style course? If so, we want to suppress showing the top-level
# maps in their own folders, in favor of "inlining" them.
my $topResource = $navmap->getById("0.0");
# Begin the HTML table
# four cols: resource + indent, chat+feedback, icon, text string
$r->print('' ."\n");
# This needs to be updated to use symbs from the remote,
# instead of uris. The changes to this and the main rendering
# loop should be obvious.
# Here's a simple example of the iterator.
# Preprocess the map: Look for current URL, force inlined maps to display
my $mapIterator = $navmap->getIterator(undef, undef, \%filterHash, 1);
my $found = 0;
my $depth = 1;
my $currentUrlIndex = 0; # keeps track of when the current resource is found,
# so we can back up a few and put the anchor above the
# current resource
my $currentUrlDelta = 5; # change this to change how many resources are displayed
# before the current resource when using #current
$mapIterator->next(); # discard the first BEGIN_MAP
my $curRes = $mapIterator->next();
my $counter = 0;
# We only need to do this if we need to open the maps to show the
# current position
while ($depth > 0 && !$ENV{'form.alreadyHere'}) {
if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; }
if ($curRes == $mapIterator->END_MAP()) { $depth--; }
if (ref($curRes)) { $counter++; }
my $mapStack = $mapIterator->getStack();
if ($currenturl && !$ENV{'form.alreadyHere'} && ref($curRes) &&
$curRes->src() eq $currenturl) {
# If this is the correct resource, be sure to
# show it by making sure the containing maps
# are open.
# This is why we have to use the main iterator instead of the
# potentially faster DFS: The count has to be the same, so
# the order has to be the same, which DFS won't give us.
$currentUrlIndex = $counter;
# Ensure the parent maps are open
for my $map (@{$mapStack}) {
if ($condition) {
undef $filterHash{$map->map_pc()};
} else {
$filterHash{$map->map_pc()} = 1;
}
}
$ENV{'form.alreadyHere'} = 1;
}
$curRes = $mapIterator->next();
}
undef $res; # so we don't accidentally use it later
my $indentLevel = 0;
my $indentString = "";
my $isNewBranch = 0;
my $now = time();
my $in24Hours = $now + 24 * 60 * 60;
my $displayedHereMarker = 0;
# We know the first thing is a BEGIN_MAP (see "$self->{STARTED}"
# code in iterator->next), so ignore the first one
$mapIterator = $navmap->getIterator(undef, undef, \%filterHash,
$condition);
$mapIterator->next();
$curRes = $mapIterator->next();
$depth = 1;
my @backgroundColors = ("#FFFFFF", "#F6F6F6");
my $rowNum = 0;
$counter = 0;
# Print the 'current' anchor here if it would fall off the top
if ($currentUrlIndex - $currentUrlDelta < 0) {
$r->print('');
}
while ($depth > 0) {
if ($curRes == $mapIterator->BEGIN_MAP() ||
$curRes == $mapIterator->BEGIN_BRANCH()) {
$indentLevel++;
}
if ($curRes == $mapIterator->END_MAP() ||
$curRes == $mapIterator->END_BRANCH()) {
$indentLevel--;
}
if ($curRes == $mapIterator->BEGIN_BRANCH()) {
$isNewBranch = 1;
}
if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; }
if ($curRes == $mapIterator->END_MAP()) { $depth--; }
if (ref($curRes)) { $counter++; }
# Is this resource being ignored because it is in a random-out
# map and it was not selected?
if (ref($curRes) && !advancedUser() && $curRes->randomout()) {
$curRes = $mapIterator->next();
next; # if yes, then just ignore this resource
}
if (ref($curRes)) {
my $deltalevel = $isNewBranch? 1 : 0; # reserves space for branch icon
if ($indentLevel - $deltalevel < 0) {
# If this would be at a negative depth (top-level maps in
# new-style courses, we want to suppress their title display)
# then ignore it.
$curRes = $mapIterator->next();
next;
}
# Step one: Decide which parts to show
my @parts = @{$curRes->parts()};
my $multipart = scalar(@parts) > 1;
my $condensed = 0;
if ($curRes->is_problem()) {
# Is it multipart?
if ($multipart) {
# If it's multipart, see if part 0 is "open"
# if it is, display all parts, if it isn't,
# just display first
if (!$curRes->opendate("0")) {
# no parts are open, display as one part
@parts = ("0");
$condensed = 1;
} else {
# Otherwise, only display part 0 if we want to
# attach feedback or email information to it
if ($curRes->hasDiscussion() || $curRes->getFeedback()) {
shift @parts;
} else {
# Now, we decide whether to condense the
# parts due to similarity
my $status = $curRes->status($parts[1]);
my $due = $curRes->duedate($parts[1]);
my $open = $curRes->opendate($parts[1]);
my $statusAllSame = 1;
my $dueAllSame = 1;
my $openAllSame = 1;
for (my $i = 2; $i < scalar(@parts); $i++) {
if ($curRes->status($parts[$i]) != $status){
$statusAllSame = 0;
}
if ($curRes->duedate($parts[$i]) != $due ) {
$dueAllSame = 0;
}
if ($curRes->opendate($parts[$i]) != $open) {
$openAllSame = 0;
}
}
# $allSame is true if all the statuses were
# the same. Now, if they are all the same and
# match one of the statuses to condense, or they
# are all open with the same due date, or they are
# all OPEN_LATER with the same open date, display the
# status of the first non-zero part (to get the 'correct'
# status right, since 0 is never 'correct' or 'open').
if (($statusAllSame && defined($condenseStatuses{$status})) ||
($dueAllSame && $status == $curRes->OPEN && $statusAllSame)||
($openAllSame && $status == $curRes->OPEN_LATER && $statusAllSame) ){
@parts = ($parts[1]);
$condensed = 1;
}
}
}
}
} else {
$parts[0] = "0"; # this is to get past foreach loop below
# you can consider a non-problem resource as a resource
# with only one part without loss, and it simplifies the looping
}
# Is it a multipart problem with a single part, now in
# @parts with "0" filtered out? If so, 'forget' it's a multi-part
# problem and treat it like a single-part problem.
if ( scalar(@parts) == 1 ) {
$multipart = 0;
}
# Display one part, in event of network error.
# If this is a single part, we can at least show the correct
# status, but if it's multipart, we're lost, since we can't
# retreive the metadata to count the parts
if ($curRes->{RESOURCE_ERROR}) {
@parts = ("0");
}
# Step Two: Print the actual data.
# For each part we intend to display...
foreach my $part (@parts) {
my $nonLinkedText = ""; # unlinked stuff after title
my $stack = $mapIterator->getStack();
my $src = getLinkForResource($stack);
# Pass the correct symb on the querystring, so the
# remote will figure out where we are if we click a link
my $srcHasQuestion = $src =~ /\?/;
my $link = $src.
($srcHasQuestion?'&':'?') .
'symb='.&Apache::lonnet::escape($curRes->symb()).
'"';
my $title = $curRes->compTitle();
my $partLabel = "";
my $newBranchText = "";
# If this is a new branch, label it so
if ($isNewBranch) {
$newBranchText = "";
$isNewBranch = 0;
}
# links to open and close the folders
my $linkopen = "";
my $linkclose = "";
my $icon = "";
if ($curRes->is_problem()) {
if ($part eq "0" || $condensed) {
$icon = '';
} else {
$icon = $indentString;
}
}
# Display the correct icon, link to open or shut map
if ($curRes->is_map()) {
my $mapId = $curRes->map_pc();
my $nowOpen = (!defined($filterHash{$mapId}));
if ($condition) {$nowOpen = !$nowOpen;}
$icon = $nowOpen ?
"navmap.folder.closed.gif" : "navmap.folder.open.gif";
$icon = "";
$linkopen = "";
$linkclose = "";
}
my $colorizer = "";
my $color;
if ($curRes->is_problem()) {
$color = $colormap{$curRes->status};
if (dueInLessThen24Hours($curRes, $part) ||
lastTry($curRes, $part)) {
$color = $hurryUpColor;
}
if ($color ne "") {
$colorizer = "bgcolor=\"$color\"";
}
}
if ($curRes->randomout()) {
$nonLinkedText .= ' (hidden) ';
}
$rowNum++;
my $backgroundColor = $backgroundColors[$rowNum % scalar(@backgroundColors)];
# FIRST COL: The resource indentation, branch icon, name, and anchor
$r->print(" \n");
# Print the anchor if necessary
if ($counter == $currentUrlIndex - $currentUrlDelta) {
$r->print('');
}
# print indentation
for (my $i = 0; $i < $indentLevel - $deltalevel; $i++) {
$r->print($indentString);
}
$r->print(" ${newBranchText}${linkopen}$icon${linkclose}\n");
my $curMarkerBegin = "";
my $curMarkerEnd = "";
# Is this the current resource?
if ($curRes->src() eq $currenturl && !$displayedHereMarker) {
$curMarkerBegin = '> ';
$curMarkerEnd = ' <';
$displayedHereMarker = 1;
}
if ($curRes->is_problem() && $part ne "0" && !$condensed) {
$partLabel = " (Part $part)";
$title = "";
}
if ($multipart && $condensed) {
$nonLinkedText .= ' (' . $curRes->countParts() . ' parts)';
}
$r->print(" $curMarkerBegin$title$partLabel $curMarkerEnd $nonLinkedText");
if ($curRes->{RESOURCE_ERROR}) {
$r->print(&Apache::loncommon::help_open_topic ("Navmap_Host_Down",
'Host down'));
}
$r->print(" | \n");
# SECOND COL: Is there text, feedback, errors??
my $discussionHTML = ""; my $feedbackHTML = "";
if ($curRes->hasDiscussion()) {
$discussionHTML = $linkopen .
'' .
$linkclose;
}
if ($curRes->getFeedback()) {
my $feedback = $curRes->getFeedback();
foreach (split(/\,/, $feedback)) {
if ($_) {
$feedbackHTML .= ' '
. '';
}
}
}
$r->print("$discussionHTML$feedbackHTML | ");
# Is this the first displayed part of a multi-part problem
# that has not been condensed, so we should suppress these two
# columns so we don't display useless status info about part
# "0"?
my $firstDisplayed = !$condensed && $multipart && $part eq "0";
# THIRD COL: Problem status icon
if ($curRes->is_problem() &&
!$firstDisplayed) {
my $icon = $statusIconMap{$curRes->status($part)};
my $alt = $iconAltTags{$icon};
if ($icon) {
$r->print("$linkopen$linkclose | \n");
} else {
$r->print(" | \n");
}
} else { # not problem, no icon
$r->print(" | \n");
}
# FOURTH COL: Text description
$r->print("\n");
if ($curRes->kind() eq "res" &&
$curRes->is_problem() &&
!$firstDisplayed) {
$r->print ("") if ($color);
$r->print (getDescription($curRes, $part));
$r->print ("") if ($color);
}
if ($curRes->is_map() && advancedUser() && $curRes->randompick()) {
$r->print('(randomly select ' . $curRes->randompick() .')');
}
$r->print(" |
\n");
if (!($counter % 20)) { $r->rflush(); }
if ($counter == 2) { $r->rflush(); }
}
}
$curRes = $mapIterator->next();
}
$r->print("