version 1.3, 2001/01/06 19:59:29
|
version 1.178, 2003/04/22 17:54:27
|
Line 1
|
Line 1
|
# The LearningOnline Network with CAPA |
# The LearningOnline Network with CAPA |
# Navigate Maps Handler |
# Navigate Maps Handler |
# |
# |
|
# $Id$ |
|
# |
|
# 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 |
# (Page Handler |
# |
# |
# (TeX Content Handler |
# (TeX Content Handler |
Line 9
|
Line 33
|
# 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23, |
# 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) |
# 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 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 |
|
# YEAR=2003 |
|
# Jeremy Bowers ... lots of days |
|
|
package Apache::lonnavmaps; |
package Apache::lonnavmaps; |
|
|
use strict; |
use strict; |
use Apache::Constants qw(:common :http); |
use Apache::Constants qw(:common :http); |
use Apache::lonnet(); |
use Apache::loncommon(); |
use HTML::TokeParser; |
use Apache::lonmenu(); |
|
use POSIX qw (floor strftime); |
|
|
|
# symbolic constants |
|
sub SYMB { return 1; } |
|
sub URL { return 2; } |
|
sub NOTHING { return 3; } |
|
|
|
# Some data |
|
|
|
my $resObj = "Apache::lonnavmaps::resource"; |
|
|
|
# Keep these mappings in sync with lonquickgrades, which uses the colors |
|
# instead of the icons. |
|
my %statusIconMap = |
|
( $resObj->NETWORK_FAILURE => '', |
|
$resObj->NOTHING_SET => '', |
|
$resObj->CORRECT => 'navmap.correct.gif', |
|
$resObj->EXCUSED => 'navmap.correct.gif', |
|
$resObj->PAST_DUE_NO_ANSWER => 'navmap.wrong.gif', |
|
$resObj->PAST_DUE_ANSWER_LATER => 'navmap.wrong.gif', |
|
$resObj->ANSWER_OPEN => 'navmap.wrong.gif', |
|
$resObj->OPEN_LATER => '', |
|
$resObj->TRIES_LEFT => 'navmap.open.gif', |
|
$resObj->INCORRECT => 'navmap.wrong.gif', |
|
$resObj->OPEN => 'navmap.open.gif', |
|
$resObj->ATTEMPTED => 'navmap.open.gif' ); |
|
|
|
my %iconAltTags = |
|
( 'navmap.correct.gif' => 'Correct', |
|
'navmap.wrong.gif' => 'Incorrect', |
|
'navmap.open.gif' => 'Open' ); |
|
|
|
# Defines a status->color mapping, null string means don't color |
|
my %colormap = |
|
( $resObj->NETWORK_FAILURE => '', |
|
$resObj->CORRECT => '', |
|
$resObj->EXCUSED => '#3333FF', |
|
$resObj->PAST_DUE_ANSWER_LATER => '', |
|
$resObj->PAST_DUE_NO_ANSWER => '', |
|
$resObj->ANSWER_OPEN => '#006600', |
|
$resObj->OPEN_LATER => '', |
|
$resObj->TRIES_LEFT => '', |
|
$resObj->INCORRECT => '', |
|
$resObj->OPEN => '', |
|
$resObj->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"; |
|
|
|
sub handler { |
|
my $r = shift; |
|
real_handler($r); |
|
} |
|
|
|
sub real_handler { |
|
my $r = shift; |
|
|
|
# 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("<html><head>\n"); |
|
$r->print("<title>Navigate Course Contents</title>"); |
|
# ------------------------------------------------------------ Get query string |
|
&Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'},['register']); |
|
|
|
# ----------------------------------------------------- Force menu registration |
|
my $addentries=''; |
|
if ($ENV{'form.register'}) { |
|
$addentries=' onLoad="'.&Apache::lonmenu::loadevents(). |
|
'" onUnload="'.&Apache::lonmenu::unloadevents().'"'; |
|
$r->print(&Apache::lonmenu::registerurl(1)); |
|
} |
|
|
|
# Header |
|
$r->print('</head>'. |
|
&Apache::loncommon::bodytag('Navigate Course Contents','', |
|
$addentries,'','',$ENV{'form.register'})); |
|
$r->print('<script>window.focus();</script>'); |
|
|
|
$r->rflush(); |
|
|
|
# Now that we've displayed some stuff to the user, init the navmap |
|
$navmap->init(); |
|
|
|
$r->print('<br> '); |
|
$r->rflush(); |
|
|
|
# Check that it's defined |
|
if (!($navmap->courseMapDefined())) { |
|
$r->print('<font size="+2" color="red">Coursemap undefined.</font>' . |
|
'</body></html>'); |
|
return OK; |
|
} |
|
|
|
# See if there's only one map in the top-level, if we don't |
|
# already have a filter... if so, automatically display it |
|
if (!defined($ENV{'form.filter'})) { |
|
my $iterator = $navmap->getIterator(undef, undef, undef, 0); |
|
my $depth = 1; |
|
$iterator->next(); |
|
my $curRes = $iterator->next(); |
|
my $sequenceCount = 0; |
|
my $sequenceId; |
|
while ($depth > 0) { |
|
if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } |
|
if ($curRes == $iterator->END_MAP()) { $depth--; } |
|
|
|
if (ref($curRes) && $curRes->is_sequence()) { |
|
$sequenceCount++; |
|
$sequenceId = $curRes->map_pc(); |
|
} |
|
|
|
$curRes = $iterator->next(); |
|
} |
|
|
|
if ($sequenceCount == 1) { |
|
# The automatic iterator creation in the render call |
|
# will pick this up. We know the condition because |
|
# the defined($ENV{'form.filter'}) also ensures this |
|
# is a fresh call. |
|
$ENV{'form.filter'} = "$sequenceId"; |
|
} |
|
} |
|
|
|
# renderer call |
|
my $render = render({ 'cols' => [0,1,2,3], |
|
'url' => '/adm/navmaps', |
|
'navmap' => $navmap, |
|
'suppressNavmap' => 1, |
|
'r' => $r}); |
|
|
|
$navmap->untieHashes(); |
|
|
|
$r->print("</body></html>"); |
|
$r->rflush(); |
|
|
|
return OK; |
|
} |
|
|
|
# Convenience functions: Returns a string that adds or subtracts |
|
# the second argument from the first hash, appropriate for the |
|
# query string that determines which folders to recurse on |
|
sub addToFilter { |
|
my $hashIn = shift; |
|
my $addition = shift; |
|
my %hash = %$hashIn; |
|
$hash{$addition} = 1; |
|
|
|
return join (",", keys(%hash)); |
|
} |
|
|
|
sub removeFromFilter { |
|
my $hashIn = shift; |
|
my $subtraction = shift; |
|
my %hash = %$hashIn; |
|
|
|
delete $hash{$subtraction}; |
|
return join(",", keys(%hash)); |
|
} |
|
|
|
# Convenience function: Given a stack returned from getStack on the iterator, |
|
# return the correct src() value. |
|
# Later, this should add an anchor when we start putting anchors in pages. |
|
sub getLinkForResource { |
|
my $stack = shift; |
|
my $res; |
|
|
|
# Check to see if there are any pages in the stack |
|
foreach $res (@$stack) { |
|
if (defined($res) && $res->is_page()) { |
|
return $res->src(); |
|
} |
|
} |
|
|
|
# Failing that, return the src of the last resource that is defined |
|
# (when we first recurse on a map, it puts an undefined resource |
|
# on the bottom because $self->{HERE} isn't defined yet, and we |
|
# want the src for the map anyhow) |
|
foreach (@$stack) { |
|
if (defined($_)) { $res = $_; } |
|
} |
|
|
|
return $res->src(); |
|
} |
|
|
|
# Convenience function: This seperates the logic of how to create |
|
# the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", |
|
# etc.) into a seperate function. It takes a resource object as the |
|
# first parameter, and the part number of the resource as the second. |
|
# It's basically a big switch statement on the status of the resource. |
|
|
|
sub getDescription { |
|
my $res = shift; |
|
my $part = shift; |
|
my $status = $res->status($part); |
|
|
|
if ($status == $res->NETWORK_FAILURE) { return ""; } |
|
if ($status == $res->NOTHING_SET) { |
|
return "Not currently assigned."; |
|
} |
|
if ($status == $res->OPEN_LATER) { |
|
return "Open " . timeToHumanString($res->opendate($part)); |
|
} |
|
if ($status == $res->OPEN) { |
|
if ($res->duedate($part)) { |
|
return "Due " . timeToHumanString($res->duedate($part)); |
|
} else { |
|
return "Open, no due date"; |
|
} |
|
} |
|
if ($status == $res->PAST_DUE_ANSWER_LATER) { |
|
return "Answer open " . timeToHumanString($res->answerdate($part)); |
|
} |
|
if ($status == $res->PAST_DUE_NO_ANSWER) { |
|
return "Was due " . timeToHumanString($res->duedate($part)); |
|
} |
|
if ($status == $res->ANSWER_OPEN) { |
|
return "Answer available"; |
|
} |
|
if ($status == $res->EXCUSED) { |
|
return "Excused by instructor"; |
|
} |
|
if ($status == $res->ATTEMPTED) { |
|
return "Not yet graded."; |
|
} |
|
if ($status == $res->TRIES_LEFT) { |
|
my $tries = $res->tries($part); |
|
my $maxtries = $res->maxtries($part); |
|
my $triesString = ""; |
|
if ($tries && $maxtries) { |
|
$triesString = "<font size=\"-1\"><i>($tries of $maxtries tries used)</i></font>"; |
|
if ($maxtries > 1 && $maxtries - $tries == 1) { |
|
$triesString = "<b>$triesString</b>"; |
|
} |
|
} |
|
if ($res->duedate()) { |
|
return "Due " . timeToHumanString($res->duedate($part)) . |
|
" $triesString"; |
|
} else { |
|
return "No due date $triesString"; |
|
} |
|
} |
|
} |
|
|
|
# Convenience function, so others can use it: Is the problem due in less then |
|
# 24 hours, and still can be done? |
|
|
|
sub dueInLessThen24Hours { |
|
my $res = shift; |
|
my $part = shift; |
|
my $status = $res->status($part); |
|
|
|
return ($status == $res->OPEN() || $status == $res->ATTEMPTED() || |
|
$status == $res->TRIES_LEFT()) && |
|
$res->duedate() && $res->duedate() < time()+(24*60*60) && |
|
$res->duedate() > time(); |
|
} |
|
|
|
# Convenience function, so others can use it: Is there only one try remaining for the |
|
# part, with more then one try to begin with, not due yet and still can be done? |
|
sub lastTry { |
|
my $res = shift; |
|
my $part = shift; |
|
|
|
my $tries = $res->tries($part); |
|
my $maxtries = $res->maxtries($part); |
|
return $tries && $maxtries && $maxtries > 1 && |
|
$maxtries - $tries == 1 && $res->duedate() && |
|
$res->duedate() > time(); |
|
} |
|
|
|
# This puts a human-readable name on the ENV variable. |
|
|
|
sub advancedUser { |
|
return $ENV{'request.role.adv'}; |
|
} |
|
|
|
|
|
# timeToHumanString takes a time number and converts it to a |
|
# human-readable representation, meant to be used in the following |
|
# manner: |
|
# print "Due $timestring" |
|
# print "Open $timestring" |
|
# print "Answer available $timestring" |
|
# Very, very, very, VERY English-only... goodness help a localizer on |
|
# this func... |
|
sub timeToHumanString { |
|
my ($time) = @_; |
|
# zero, '0' and blank are bad times |
|
if (!$time) { |
|
return 'never'; |
|
} |
|
|
|
my $now = time(); |
|
|
|
my @time = localtime($time); |
|
my @now = localtime($now); |
|
|
|
# Positive = future |
|
my $delta = $time - $now; |
|
|
|
my $minute = 60; |
|
my $hour = 60 * $minute; |
|
my $day = 24 * $hour; |
|
my $week = 7 * $day; |
|
my $inPast = 0; |
|
|
|
# Logic in comments: |
|
# Is it now? (extremely unlikely) |
|
if ( $delta == 0 ) { |
|
return "this instant"; |
|
} |
|
|
|
if ($delta < 0) { |
|
$inPast = 1; |
|
$delta = -$delta; |
|
} |
|
|
|
if ( $delta > 0 ) { |
|
|
|
my $tense = $inPast ? " ago" : ""; |
|
my $prefix = $inPast ? "" : "in "; |
|
|
|
# Less then a minute |
|
if ( $delta < $minute ) { |
|
if ($delta == 1) { return "${prefix}1 second$tense"; } |
|
return "$prefix$delta seconds$tense"; |
|
} |
|
|
|
# Less then an hour |
|
if ( $delta < $hour ) { |
|
# If so, use minutes |
|
my $minutes = floor($delta / 60); |
|
if ($minutes == 1) { return "${prefix}1 minute$tense"; } |
|
return "$prefix$minutes minutes$tense"; |
|
} |
|
|
|
# Is it less then 24 hours away? If so, |
|
# display hours + minutes |
|
if ( $delta < $hour * 24) { |
|
my $hours = floor($delta / $hour); |
|
my $minutes = floor(($delta % $hour) / $minute); |
|
my $hourString = "$hours hours"; |
|
my $minuteString = ", $minutes minutes"; |
|
if ($hours == 1) { |
|
$hourString = "1 hour"; |
|
} |
|
if ($minutes == 1) { |
|
$minuteString = ", 1 minute"; |
|
} |
|
if ($minutes == 0) { |
|
$minuteString = ""; |
|
} |
|
return "$prefix$hourString$minuteString$tense"; |
|
} |
|
|
|
# Less then 5 days away, display day of the week and |
|
# HH:MM |
|
if ( $delta < $day * 5 ) { |
|
my $timeStr = strftime("%A, %b %e at %I:%M %P", localtime($time)); |
|
$timeStr =~ s/12:00 am/midnight/; |
|
$timeStr =~ s/12:00 pm/noon/; |
|
return ($inPast ? "last " : "next ") . |
|
$timeStr; |
|
} |
|
|
|
# Is it this year? |
|
if ( $time[5] == $now[5]) { |
|
# Return on Month Day, HH:MM meridian |
|
my $timeStr = strftime("on %A, %b %e at %I:%M %P", localtime($time)); |
|
$timeStr =~ s/12:00 am/midnight/; |
|
$timeStr =~ s/12:00 pm/noon/; |
|
return $timeStr; |
|
} |
|
|
|
# Not this year, so show the year |
|
my $timeStr = strftime("on %A, %b %e %G at %I:%M %P", localtime($time)); |
|
$timeStr =~ s/12:00 am/midnight/; |
|
$timeStr =~ s/12:00 pm/noon/; |
|
return $timeStr; |
|
} |
|
} |
|
|
|
|
|
=pod |
|
|
|
=head1 NAME |
|
|
|
Apache::lonnavmap - Subroutines to handle and render the navigation maps |
|
|
|
=head1 SYNOPSIS |
|
|
|
The main handler generates the navigational listing for the course, |
|
the other objects export this information in a usable fashion for |
|
other modules |
|
|
|
=head1 Object: render |
|
|
|
The navmap renderer package provides a sophisticated rendering of the |
|
standard navigation maps interface into HTML. The provided nav map |
|
handler is actually just a glorified call to this. |
|
|
|
Because of the large number of parameters this function presents, |
|
instead of passing it arguments as is normal, pass it in an anonymous |
|
hash with the given options. This is because there is no obvious order |
|
you may wish to override these in and a hash is easier to read and |
|
understand then "undef, undef, undef, 1, undef, undef, renderButton, |
|
undef, 0" when you mostly want default behaviors. |
|
|
|
The package provides a function called 'render', called as |
|
Apache::lonnavmaps::renderer->render({}). |
|
|
|
=head2 Overview of Columns |
|
|
|
The renderer will build an HTML table for the navmap and return |
|
it. The table is consists of several columns, and a row for each |
|
resource (or possibly each part). You tell the renderer how many |
|
columns to create and what to place in each column, optionally using |
|
one or more of the preparent columns, and the renderer will assemble |
|
the table. |
|
|
|
Any additional generally useful column types should be placed in the |
|
renderer code here, so anybody can use it anywhere else. Any code |
|
specific to the current application (such as the addition of <input> |
|
elements in a column) should be placed in the code of the thing using |
|
the renderer. |
|
|
|
At the core of the renderer is the array reference COLS (see Example |
|
section below for how to pass this correctly). The COLS array will |
|
consist of entries of one of two types of things: Either an integer |
|
representing one of the pre-packaged column types, or a sub reference |
|
that takes a resource reference, a part number, and a reference to the |
|
argument hash passed to the renderer, and returns a string that will |
|
be inserted into the HTML representation as it. |
|
|
|
The pre-packaged column names are refered to by constants in the |
|
Apache::lonnavmaps::renderer namespace. The following currently exist: |
|
|
|
=over 4 |
|
|
|
=item * B<resource>: |
|
|
|
The general info about the resource: Link, icon for the type, etc. The |
|
first column in the standard nav map display. This column also accepts |
|
the following parameter in the renderer hash: |
|
|
|
=over 4 |
|
|
|
=item * B<resource_nolink>: |
|
|
|
If true, the resource will not be linked. Default: false, resource |
|
will have links. |
|
|
|
=item * B<resource_part_count>: |
|
|
|
If true (default), the resource will show a part count if the full |
|
part list is not displayed. If false, the resource will never show a |
|
part count. |
|
|
|
=item * B<resource_no_folder_link>: |
|
|
|
If true, the resource's folder will not be clickable to open or close |
|
it. Default is false. True implies printCloseAll is false, since you |
|
can't close or open folders when this is on anyhow. |
|
|
|
=back |
|
|
|
=item B<communication_status>: |
|
|
|
Whether there is discussion on the resource, email for the user, or |
|
(lumped in here) perl errors in the execution of the problem. This is |
|
the second column in the main nav map. |
|
|
|
=item B<quick_status>: |
|
|
|
An icon for the status of a problem, with four possible states: |
|
Correct, incorrect, open, or none (not open yet, not a problem). The |
|
third column of the standard navmap. |
|
|
|
=item B<long_status>: |
|
|
|
A text readout of the details of the current status of the problem, |
|
such as "Due in 22 hours". The fourth column of the standard navmap. |
|
|
|
=back |
|
|
|
If you add any others please be sure to document them here. |
|
|
|
An example of a column renderer that will show the ID number of a |
|
resource, along with the part name if any: |
|
|
|
sub { |
|
my ($resource, $part, $params) = @_; |
|
if ($part) { return '<td>' . $resource->{ID} . ' ' . $part . '</td>'; } |
|
return '<td>' . $resource->{ID} . '</td>'; |
|
} |
|
|
|
Note these functions are responsible for the TD tags, which allow them |
|
to override vertical and horizontal alignment, etc. |
|
|
|
=head2 Parameters |
|
|
|
Most of these parameters are only useful if you are *not* using the |
|
folder interface (i.e., the default first column), which is probably |
|
the common case. If you are using this interface, then you should be |
|
able to get away with just using 'cols' (to specify the columns |
|
shown), 'url' (necessary for the folders to link to the current screen |
|
correctly), and possibly 'queryString' if your app calls for it. In |
|
that case, maintaining the state of the folders will be done |
|
automatically. |
|
|
|
=over 4 |
|
|
|
=item * B<iterator>: |
|
|
|
A reference to a fresh ::iterator to use from the navmaps. The |
|
rendering will reflect the options passed to the iterator, so you can |
|
use that to just render a certain part of the course, if you like. If |
|
one is not passed, the renderer will attempt to construct one from |
|
ENV{'form.filter'} and ENV{'form.condition'} information, plus the |
|
'iterator_map' parameter if any. |
|
|
|
=item * B<iterator_map>: |
|
|
|
If you are letting the renderer do the iterator handling, you can |
|
instruct the renderer to render only a particular map by passing it |
|
the source of the map you want to process, like |
|
'/res/103/jerf/navmap.course.sequence'. |
|
|
|
=item * B<navmap>: |
|
|
|
A reference to a navmap, used only if an iterator is not passed in. If |
|
this is necessary to make an iterator but it is not passed in, a new |
|
one will be constructed based on ENV info. This is useful to do basic |
|
error checking before passing it off to render. |
|
|
|
=item * B<r>: |
|
|
|
The standard Apache response object. This must be passed to the |
|
renderer or the course hash will be locked. |
|
|
|
=item * B<cols>: |
|
|
|
An array reference |
|
|
|
=item * B<showParts>: |
|
|
|
A flag. If yes (default), a line for the resource itself, and a line |
|
for each part will be displayed. If not, only one line for each |
|
resource will be displayed. |
|
|
|
=item * B<condenseParts>: |
|
|
|
A flag. If yes (default), if all parts of the problem have the same |
|
status and that status is Nothing Set, Correct, or Network Failure, |
|
then only one line will be displayed for that resource anyhow. If no, |
|
all parts will always be displayed. If showParts is 0, this is |
|
ignored. |
|
|
|
=item * B<jumpCount>: |
|
|
|
A string identifying the URL to place the anchor 'curloc' at. Default |
|
to no anchor at all. It is the responsibility of the renderer user to |
|
ensure that the #curloc is in the URL. By default, determined through |
|
the use of the ENV{} 'jump' information, and should normally "just |
|
work" correctly. |
|
|
|
=item * B<here>: |
|
|
|
A Symb identifying where to place the 'here' marker. Default empty, |
|
which means no marker. |
|
|
|
=item * B<indentString>: |
|
|
|
A string identifying the indentation string to use. By default, this |
|
is a 25 pixel whitespace image with no alt text. |
|
|
|
=item * B<queryString>: |
|
|
|
A string which will be prepended to the query string used when the |
|
folders are opened or closed. |
|
|
|
=item * B<url>: |
|
|
|
The url the folders will link to, which should be the current |
|
page. Required if the resource info column is shown. |
|
|
|
=item * B<currentJumpIndex>: |
|
|
|
Describes the currently-open row number to cause the browser to jump |
|
to, because the user just opened that folder. By default, pulled from |
|
the Jump information in the ENV{'form.*'}. |
|
|
|
=item * B<printKey>: |
|
|
|
If true, print the key that appears on the top of the standard |
|
navmaps. Default is false. |
|
|
|
=item * B<printCloseAll>: |
|
|
|
If true, print the "Close all folders" or "open all folders" |
|
links. Default is true. |
|
|
|
=item * B<filterFunc>: |
|
|
|
A function that takes the resource object as its only parameter and |
|
returns a true or false value. If true, the resource is displayed. If |
|
false, it is simply skipped in the display. By default, all resources |
|
are shown. |
|
|
|
=item * B<suppressNavmaps>: |
|
|
|
If true, will not display Navigate Content resources. Default to |
|
false. |
|
|
|
=back |
|
|
|
=head2 Additional Info |
|
|
|
In addition to the parameters you can pass to the renderer, which will |
|
be passed through unchange to the column renderers, the renderer will |
|
generate the following information which your renderer may find |
|
useful: |
|
|
|
If you want to know how many rows were printed, the 'counter' element |
|
of the hash passed into the render function will contain the |
|
count. You may want to check whether any resources were printed at |
|
all. |
|
|
|
=over 4 |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub resource { return 0; } |
|
sub communication_status { return 1; } |
|
sub quick_status { return 2; } |
|
sub long_status { return 3; } |
|
|
|
# Data for render_resource |
|
|
|
sub render_resource { |
|
my ($resource, $part, $params) = @_; |
|
|
|
my $nonLinkedText = ''; # stuff after resource title not in link |
|
|
|
my $link = $params->{"resourceLink"}; |
|
my $src = $resource->src(); |
|
my $it = $params->{"iterator"}; |
|
my $filter = $it->{FILTER}; |
|
|
|
my $title = $resource->compTitle(); |
|
if ($src =~ /^\/uploaded\//) { |
|
$nonLinkedText=$title; |
|
$title = ''; |
|
} |
|
my $partLabel = ""; |
|
my $newBranchText = ""; |
|
|
|
# If this is a new branch, label it so |
|
if ($params->{'isNewBranch'}) { |
|
$newBranchText = "<img src='/adm/lonIcons/branch.gif' border='0' />"; |
|
} |
|
|
|
# links to open and close the folder |
|
my $linkopen = "<a href='$link'>"; |
|
my $linkclose = "</a>"; |
|
|
|
# Default icon: HTML page |
|
my $icon = "<img src='/adm/lonIcons/html.gif' alt='' border='0' />"; |
|
|
|
if ($resource->is_problem()) { |
|
if ($part eq "" || $params->{'condensed'}) { |
|
$icon = '<img src="/adm/lonIcons/problem.gif" alt="" border="0" />'; |
|
} else { |
|
$icon = $params->{'indentString'}; |
|
} |
|
} |
|
|
|
# Display the correct map icon to open or shut map |
|
if ($resource->is_map()) { |
|
my $mapId = $resource->map_pc(); |
|
my $nowOpen = !defined($filter->{$mapId}); |
|
if ($it->{CONDITION}) { |
|
$nowOpen = !$nowOpen; |
|
} |
|
|
|
if (!$params->{'resource_no_folder_link'}) { |
|
$icon = 'navmap.folder.' . ($nowOpen ? 'closed' : 'open') . '.gif'; |
|
$icon = "<img src='/adm/lonIcons/$icon' alt='' border='0' />"; |
|
|
|
$linkopen = "<a href='" . $params->{'url'} . '?' . |
|
$params->{'queryString'} . '&filter='; |
|
$linkopen .= ($nowOpen xor $it->{CONDITION}) ? |
|
addToFilter($filter, $mapId) : |
|
removeFromFilter($filter, $mapId); |
|
$linkopen .= "&condition=" . $it->{CONDITION} . '&hereType=' |
|
. $params->{'hereType'} . '&here=' . |
|
&Apache::lonnet::escape($params->{'here'}) . |
|
'&jump=' . |
|
&Apache::lonnet::escape($resource->symb()) . |
|
"&folderManip=1'>"; |
|
} else { |
|
# Don't allow users to manipulate folder |
|
$icon = 'navmap.folder.' . ($nowOpen ? 'closed' : 'open') . |
|
'.nomanip.gif'; |
|
$icon = "<img src='/adm/lonIcons/$icon' alt='' border='0' />"; |
|
|
|
$linkopen = ""; |
|
$linkclose = ""; |
|
} |
|
} |
|
|
|
if ($resource->randomout()) { |
|
$nonLinkedText .= ' <i>(hidden)</i> '; |
|
} |
|
|
|
# We're done preparing and finally ready to start the rendering |
|
my $result = "<td align='left' valign='center'>"; |
|
|
|
my $indentLevel = $params->{'indentLevel'}; |
|
if ($newBranchText) { $indentLevel--; } |
|
|
|
# print indentation |
|
for (my $i = 0; $i < $indentLevel; $i++) { |
|
$result .= $params->{'indentString'}; |
|
} |
|
|
|
# Decide what to display |
|
$result .= "$newBranchText$linkopen$icon$linkclose"; |
|
|
|
my $curMarkerBegin = ''; |
|
my $curMarkerEnd = ''; |
|
|
|
# Is this the current resource? |
|
if (!$params->{'displayedHereMarker'} && |
|
$resource->symb() eq $params->{'here'} ) { |
|
$curMarkerBegin = '<font color="red" size="+2">> </font>'; |
|
$curMarkerEnd = '<font color="red" size="+2"><</font>'; |
|
$params->{'displayedHereMarker'} = 1; |
|
} |
|
|
|
if ($resource->is_problem() && $part ne "" && |
|
!$params->{'condensed'}) { |
|
$partLabel = " (Part $part)"; |
|
$title = ""; |
|
} |
|
|
|
if ($params->{'condensed'} && $resource->countParts() > 1) { |
|
$nonLinkedText .= ' (' . $resource->countParts() . ' parts)'; |
|
} |
|
|
|
if (!$params->{'resource_nolink'}) { |
|
$result .= " $curMarkerBegin<a href='$link'>$title$partLabel</a>$curMarkerEnd $nonLinkedText</td>"; |
|
} else { |
|
$result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText</td>"; |
|
} |
|
|
|
return $result; |
|
} |
|
|
|
sub render_communication_status { |
|
my ($resource, $part, $params) = @_; |
|
my $discussionHTML = ""; my $feedbackHTML = ""; my $errorHTML = ""; |
|
|
|
my $link = $params->{"resourceLink"}; |
|
my $linkopen = "<a href='$link'>"; |
|
my $linkclose = "</a>"; |
|
|
|
if ($resource->hasDiscussion()) { |
|
$discussionHTML = $linkopen . |
|
'<img border="0" src="/adm/lonMisc/chat.gif" />' . |
|
$linkclose; |
|
} |
|
|
|
if ($resource->getFeedback()) { |
|
my $feedback = $resource->getFeedback(); |
|
foreach (split(/\,/, $feedback)) { |
|
if ($_) { |
|
$feedbackHTML .= ' <a href="/adm/email?display=' |
|
. &Apache::lonnet::escape($_) . '">' |
|
. '<img src="/adm/lonMisc/feedback.gif" ' |
|
. 'border="0" /></a>'; |
|
} |
|
} |
|
} |
|
|
|
if ($resource->getErrors()) { |
|
my $errors = $resource->getErrors(); |
|
foreach (split(/,/, $errors)) { |
|
if ($_) { |
|
$errorHTML .= ' <a href="/adm/email?display=' |
|
. &Apache::lonnet::escape($_) . '">' |
|
. '<img src="/adm/lonMisc/bomb.gif" ' |
|
. 'border="0" /></a>'; |
|
} |
|
} |
|
} |
|
|
|
return "<td width=\"75\" align=\"left\" valign=\"center\">$discussionHTML$feedbackHTML$errorHTML </td>"; |
|
|
|
} |
|
sub render_quick_status { |
|
my ($resource, $part, $params) = @_; |
|
my $result = ""; |
|
my $firstDisplayed = !$params->{'condensed'} && |
|
$params->{'multipart'} && $part eq "0"; |
|
|
|
my $link = $params->{"resourceLink"}; |
|
my $linkopen = "<a href='$link'>"; |
|
my $linkclose = "</a>"; |
|
|
|
if ($resource->is_problem() && |
|
!$firstDisplayed) { |
|
my $icon = $statusIconMap{$resource->status($part)}; |
|
my $alt = $iconAltTags{$icon}; |
|
if ($icon) { |
|
$result .= "<td width='30' valign='center' width='50' align='right'>$linkopen<img width='25' height='25' src='/adm/lonIcons/$icon' border='0' alt='$alt' />$linkclose</td>\n"; |
|
} else { |
|
$result .= "<td width='30'> </td>\n"; |
|
} |
|
} else { # not problem, no icon |
|
$result .= "<td width='30'> </td>\n"; |
|
} |
|
|
|
return $result; |
|
} |
|
sub render_long_status { |
|
my ($resource, $part, $params) = @_; |
|
my $result = "<td align='right' valign='center'>\n"; |
|
my $firstDisplayed = !$params->{'condensed'} && |
|
$params->{'multipart'} && $part eq "0"; |
|
|
|
my $color; |
|
if ($resource->is_problem()) { |
|
$color = $colormap{$resource->status}; |
|
|
|
if (dueInLessThen24Hours($resource, $part) || |
|
lastTry($resource, $part)) { |
|
$color = $hurryUpColor; |
|
} |
|
} |
|
|
|
if ($resource->kind() eq "res" && |
|
$resource->is_problem() && |
|
!$firstDisplayed) { |
|
if ($color) {$result .= "<font color=\"$color\"><b>"; } |
|
$result .= getDescription($resource, $part); |
|
if ($color) {$result .= "</b></font>"; } |
|
} |
|
if ($resource->is_map() && advancedUser() && $resource->randompick()) { |
|
$result .= '(randomly select ' . $resource->randompick() .')'; |
|
} |
|
|
|
$result .= " </td>\n"; |
|
|
|
return $result; |
|
} |
|
|
|
my @preparedColumns = (\&render_resource, \&render_communication_status, |
|
\&render_quick_status, \&render_long_status); |
|
|
|
sub setDefault { |
|
my ($val, $default) = @_; |
|
if (!defined($val)) { return $default; } |
|
return $val; |
|
} |
|
|
|
sub render { |
|
my $args = shift; |
|
&Apache::loncommon::get_unprocessed_cgi($ENV{QUERY_STRING}); |
|
my $result = ''; |
|
|
|
# Configure the renderer. |
|
my $cols = $args->{'cols'}; |
|
if (!defined($cols)) { |
|
# no columns, no nav maps. |
|
return ''; |
|
} |
|
my $mustCloseNavMap = 0; |
|
my $navmap; |
|
if (defined($args->{'navmap'})) { |
|
$navmap = $args->{'navmap'}; |
|
} |
|
|
|
my $r = $args->{'r'}; |
|
my $queryString = $args->{'queryString'}; |
|
my $jump = $args->{'jump'}; |
|
my $here = $args->{'here'}; |
|
my $suppressNavmap = setDefault($args->{'suppressNavmap'}, 0); |
|
my $currentJumpDelta = 2; # change this to change how many resources are displayed |
|
# before the current resource when using #current |
|
|
|
# If we were passed 'here' information, we are not rendering |
|
# after a folder manipulation, and we were not passed an |
|
# iterator, make sure we open the folders to show the "here" |
|
# marker |
|
my $filterHash = {}; |
|
# Figure out what we're not displaying |
|
foreach (split(/\,/, $ENV{"form.filter"})) { |
|
if ($_) { |
|
$filterHash->{$_} = "1"; |
|
} |
|
} |
|
|
|
my $condition = 0; |
|
if ($ENV{'form.condition'}) { |
|
$condition = 1; |
|
} |
|
|
|
if (!$ENV{'form.folderManip'} && !defined($args->{'iterator'})) { |
|
# Step 1: Check to see if we have a navmap |
|
if (!defined($navmap)) { |
|
$navmap = Apache::lonnavmaps::navmap->new( |
|
$ENV{"request.course.fn"}.".db", |
|
$ENV{"request.course.fn"}."_parms.db", 1, 1); |
|
$mustCloseNavMap = 1; |
|
} |
|
$navmap->init(); |
|
|
|
# Step two: Locate what kind of here marker is necessary |
|
# Determine where the "here" marker is and where the screen jumps to. |
|
|
|
if ($ENV{'form.postsymb'}) { |
|
$here = $jump = $ENV{'form.postsymb'}; |
|
} elsif ($ENV{'form.postdata'}) { |
|
# couldn't find a symb, is there a URL? |
|
my $currenturl = $ENV{'form.postdata'}; |
|
#$currenturl=~s/^http\:\/\///; |
|
#$currenturl=~s/^[^\/]+//; |
|
|
|
$here = $jump = &Apache::lonnet::symbread($currenturl); |
|
} |
|
|
|
# Step three: Ensure the folders are open |
|
my $mapIterator = $navmap->getIterator(undef, undef, undef, 1); |
|
my $depth = 1; |
|
$mapIterator->next(); # discard the first BEGIN_MAP |
|
my $curRes = $mapIterator->next(); |
|
my $found = 0; |
|
|
|
# We only need to do this if we need to open the maps to show the |
|
# current position. This will change the counter so we can't count |
|
# for the jump marker with this loop. |
|
while ($depth > 0 && !$found) { |
|
if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } |
|
if ($curRes == $mapIterator->END_MAP()) { $depth--; } |
|
|
|
if (ref($curRes) && $curRes->symb() eq $here) { |
|
my $mapStack = $mapIterator->getStack(); |
|
|
|
# Ensure the parent maps are open |
|
for my $map (@{$mapStack}) { |
|
if ($condition) { |
|
undef $filterHash->{$map->map_pc()}; |
|
} else { |
|
$filterHash->{$map->map_pc()} = 1; |
|
} |
|
} |
|
$found = 1; |
|
} |
|
|
|
$curRes = $mapIterator->next(); |
|
} |
|
} |
|
|
|
if ( !defined($args->{'iterator'}) && $ENV{'form.folderManip'} ) { # we came from a user's manipulation of the nav page |
|
# If this is a click on a folder or something, we want to preserve the "here" |
|
# from the querystring, and get the new "jump" marker |
|
$here = $ENV{'form.here'}; |
|
$jump = $ENV{'form.jump'}; |
|
} |
|
|
|
my $it = $args->{'iterator'}; |
|
if (!defined($it)) { |
|
# Construct a default iterator based on $ENV{'form.'} information |
|
|
|
# Step 1: Check to see if we have a navmap |
|
if (!defined($navmap)) { |
|
$navmap = Apache::lonnavmaps::navmap->new($r, |
|
$ENV{"request.course.fn"}.".db", |
|
$ENV{"request.course.fn"}."_parms.db", 1, 1); |
|
$mustCloseNavMap = 1; |
|
} |
|
# Paranoia: Make sure it's ready |
|
$navmap->init(); |
|
|
|
# See if we're being passed a specific map |
|
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); |
|
} else { |
|
$args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition); |
|
} |
|
} |
|
|
|
# (re-)Locate the jump point, if any |
|
my $mapIterator = $navmap->getIterator(undef, undef, $filterHash, 0); |
|
my $depth = 1; |
|
$mapIterator->next(); |
|
my $curRes = $mapIterator->next(); |
|
my $foundJump = 0; |
|
my $counter = 0; |
|
|
|
while ($depth > 0 && !$foundJump) { |
|
if ($curRes == $mapIterator->BEGIN_MAP()) { $depth++; } |
|
if ($curRes == $mapIterator->END_MAP()) { $depth--; } |
|
if (ref($curRes)) { $counter++; } |
|
|
|
if (ref($curRes) && $jump eq $curRes->symb()) { |
|
|
|
# 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. |
|
$args->{'currentJumpIndex'} = $counter; |
|
$foundJump = 1; |
|
} |
|
|
|
$curRes = $mapIterator->next(); |
|
} |
|
|
|
my $showParts = setDefault($args->{'showParts'}, 1); |
|
my $condenseParts = setDefault($args->{'condenseParts'}, 1); |
|
# 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 $printKey = $args->{'printKey'}; |
|
my $printCloseAll = $args->{'printCloseAll'}; |
|
if (!defined($printCloseAll)) { $printCloseAll = 1; } |
|
my $filterFunc = setDefault($args->{'filterFunc'}, |
|
sub {return 1;}); |
|
|
|
# Print key? |
|
if ($printKey) { |
|
$result .= '<table border="0" cellpadding="2" cellspacing="0">'; |
|
my $date=localtime; |
|
$result.='<tr><td align="right" valign="bottom">Key: </td>'; |
|
if ($navmap->{LAST_CHECK}) { |
|
$result .= |
|
'<img src="/adm/lonMisc/chat.gif"> New discussion since '. |
|
strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). |
|
'</td><td align="center" valign="bottom"> '. |
|
'<img src="/adm/lonMisc/feedback.gif"> New message (click to open)<p>'. |
|
'</td>'; |
|
} else { |
|
$result .= '<td align="center" valign="bottom"> '. |
|
'<img src="/adm/lonMisc/chat.gif"> Discussions</td><td align="center" valign="bottom">'. |
|
' <img src="/adm/lonMisc/feedback.gif"> New message (click to open)'. |
|
'</td>'; |
|
} |
|
|
|
$result .= '</tr></table>'; |
|
} |
|
|
|
if ($printCloseAll && !$args->{'resource_no_folder_link'}) { |
|
if ($condition) { |
|
$result.="<a href=\"navmaps?condition=0&filter=&$queryString" . |
|
"&here=" . Apache::lonnet::escape($here) . |
|
"\">Close All Folders</a>"; |
|
} else { |
|
$result.="<a href=\"navmaps?condition=1&filter=&$queryString" . |
|
"&here=" . Apache::lonnet::escape($here) . |
|
"\">Open All Folders</a>"; |
|
} |
|
$result .= "<br /><br />\n"; |
|
} |
|
|
|
if ($r) { |
|
$r->print($result); |
|
$r->rflush(); |
|
$result = ""; |
|
} |
|
# End parameter setting |
|
|
|
# Data |
|
$result .= '<table cellspacing="0" cellpadding="3" border="0" bgcolor="#FFFFFF">' ."\n"; |
|
my $res = "Apache::lonnavmaps::resource"; |
|
my %condenseStatuses = |
|
( $res->NETWORK_FAILURE => 1, |
|
$res->NOTHING_SET => 1, |
|
$res->CORRECT => 1 ); |
|
my @backgroundColors = ("#FFFFFF", "#F6F6F6"); |
|
|
|
# Shared variables |
|
$args->{'counter'} = 0; # counts the rows |
|
$args->{'indentLevel'} = 0; |
|
$args->{'isNewBranch'} = 0; |
|
$args->{'condensed'} = 0; |
|
$args->{'indentString'} = setDefault($args->{'indentString'}, "<img src='/adm/lonIcons/whitespace1.gif' width='25' height='1' alt='' border='0' />"); |
|
$args->{'displayedHereMarker'} = 0; |
|
|
|
my $displayedJumpMarker = 0; |
|
# Set up iteration. |
|
$depth = 1; |
|
$it->next(); # discard initial BEGIN_MAP |
|
$curRes = $it->next(); |
|
my $now = time(); |
|
my $in24Hours = $now + 24 * 60 * 60; |
|
my $rownum = 0; |
|
|
|
# export "here" marker information |
|
$args->{'here'} = $here; |
|
|
|
while ($depth > 0) { |
|
if ($curRes == $it->BEGIN_MAP()) { $depth++; } |
|
if ($curRes == $it->END_MAP()) { $depth--; } |
|
|
|
# Maintain indentation level. |
|
if ($curRes == $it->BEGIN_MAP() || |
|
$curRes == $it->BEGIN_BRANCH() ) { |
|
$args->{'indentLevel'}++; |
|
} |
|
if ($curRes == $it->END_MAP() || |
|
$curRes == $it->END_BRANCH() ) { |
|
$args->{'indentLevel'}--; |
|
} |
|
# Notice new branches |
|
if ($curRes == $it->BEGIN_BRANCH()) { |
|
$args->{'isNewBranch'} = 1; |
|
} |
|
|
|
# If this isn't an actual resource, continue on |
|
if (!ref($curRes)) { |
|
next; |
|
} |
|
|
|
$args->{'counter'}++; |
|
|
|
# If this has been filtered out, continue on |
|
if (!(&$filterFunc($curRes))) { |
|
$args->{'isNewBranch'} = 0; # Don't falsely remember this |
|
next; |
|
} |
|
|
|
# If we're suppressing navmaps and this is a navmap, continue on |
|
if ($suppressNavmap && $curRes->src() =~ /^\/adm\/navmaps/) { |
|
next; |
|
} |
|
|
|
# Does it have multiple parts? |
|
$args->{'multipart'} = 0; |
|
$args->{'condensed'} = 0; |
|
my @parts; |
|
|
|
# Decide what parts to show. |
|
if ($curRes->is_problem() && $showParts) { |
|
@parts = @{$curRes->parts()}; |
|
$args->{'multipart'} = scalar(@parts) > 1; |
|
|
|
if ($condenseParts) { # do the condensation |
|
if (!$curRes->opendate("0")) { |
|
@parts = (); |
|
$args->{'condensed'} = 1; |
|
} |
|
if (!$args->{'condensed'}) { |
|
# Decide whether to condense based on 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 = (); |
|
$args->{'condensed'} = 1; |
|
} |
|
|
|
} |
|
} |
|
} |
|
|
|
# If the multipart problem was condensed, "forget" it was multipart |
|
if (scalar(@parts) == 1) { |
|
$args->{'multipart'} = 0; |
|
} |
|
|
|
# Now, we've decided what parts to show. Loop through them and |
|
# show them. |
|
foreach my $part ('', @parts) { |
|
if ($part eq '0') { |
|
next; |
|
} |
|
$rownum ++; |
|
my $backgroundColor = $backgroundColors[$rownum % scalar(@backgroundColors)]; |
|
|
|
$result .= " <tr bgcolor='$backgroundColor'>\n"; |
|
|
|
# Set up some data about the parts that the cols might want |
|
my $filter = $it->{FILTER}; |
|
my $stack = $it->getStack(); |
|
my $src = getLinkForResource($stack); |
|
|
|
my $srcHasQuestion = $src =~ /\?/; |
|
$args->{"resourceLink"} = $src. |
|
($srcHasQuestion?'&':'?') . |
|
'symb=' . &Apache::lonnet::escape($curRes->symb()); |
|
|
|
# Now, display each column. |
|
foreach my $col (@$cols) { |
|
my $colHTML = ''; |
|
if (ref($col)) { |
|
$colHTML .= &$col($curRes, $part, $args); |
|
} else { |
|
$colHTML .= &{$preparedColumns[$col]}($curRes, $part, $args); |
|
} |
|
|
|
# If this is the first column and it's time to print |
|
# the anchor, do so |
|
if ($col == $cols->[0] && |
|
$args->{'counter'} == $args->{'currentJumpIndex'} - |
|
$currentJumpDelta) { |
|
# Jam the anchor after the <td> tag; |
|
# necessary for valid HTML (which Mozilla requires) |
|
$colHTML =~ s/\>/\>\<a name="curloc" \/\>/; |
|
$displayedJumpMarker = 1; |
|
} |
|
$result .= $colHTML . "\n"; |
|
} |
|
$result .= " </tr>\n"; |
|
$args->{'isNewBranch'} = 0; |
|
} |
|
|
|
if ($r && $rownum % 20 == 0) { |
|
$r->print($result); |
|
$result = ""; |
|
$r->rflush(); |
|
} |
|
} continue { |
|
$curRes = $it->next(); |
|
} |
|
|
|
# Print out the part that jumps to #curloc if it exists |
|
# delay needed because the browser is processing the jump before |
|
# it finishes rendering, so it goes to the wrong place! |
|
# onload might be better, but this routine has no access to that. |
|
# On mozilla, the 0-millisecond timeout seems to prevent this; |
|
# it's quite likely this might fix other browsers, too, and |
|
# certainly won't hurt anything. |
|
if ($displayedJumpMarker) { |
|
$result .= "<script>setTimeout(\"location += '#curloc';\", 0)</script>\n"; |
|
} |
|
|
|
$result .= "</table>"; |
|
|
|
if ($r) { |
|
$r->print($result); |
|
$result = ""; |
|
$r->rflush(); |
|
} |
|
|
|
if ($mustCloseNavMap) { $navmap->untieHashes(); } |
|
|
|
return $result; |
|
} |
|
|
|
1; |
|
|
|
package Apache::lonnavmaps::navmap; |
|
|
|
=pod |
|
|
|
lonnavmaps provides functions and objects for dealing with the |
|
compiled course hashes generated when a user enters the course, the |
|
Apache handler for the "Navigation Map" button, and a flexible |
|
prepared renderer for navigation maps that are easy to use anywhere. |
|
|
|
=head1 Object: navmap |
|
|
|
Encapsulating the compiled nav map |
|
|
|
navmap is an object that encapsulates a compiled course map and |
|
provides a reasonable interface to it. |
|
|
|
Most notably it provides a way to navigate the map sensibly and a |
|
flexible iterator that makes it easy to write various renderers based |
|
on nav maps. |
|
|
|
You must obtain resource objects through the navmap object. |
|
|
|
=head2 Methods |
|
|
|
=over 4 |
|
|
|
=item * B<new>(navHashFile, parmHashFile, genCourseAndUserOptions, |
|
genMailDiscussStatus): |
|
|
|
Binds a new navmap object to the compiled nav map hash and parm hash |
|
given as filenames. genCourseAndUserOptions is a flag saying whether |
|
the course options and user options hash should be generated. This is |
|
for when you are using the parameters of the resources that require |
|
them; see documentation in resource object |
|
documentation. genMailDiscussStatus causes the nav map to retreive |
|
information about the email and discussion status of |
|
resources. Returns the navmap object if this is successful, or |
|
B<undef> if not. You must check for undef; errors will occur when you |
|
try to use the other methods otherwise. |
|
|
|
=item * B<getIterator>(first, finish, filter, condition): |
|
|
|
See iterator documentation below. |
|
|
|
=cut |
|
|
|
use strict; |
use GDBM_File; |
use GDBM_File; |
|
|
# -------------------------------------------------------------- Module Globals |
sub new { |
my %hash; |
# magic invocation to create a class instance |
my @rows; |
my $proto = shift; |
|
my $class = ref($proto) || $proto; |
# ------------------------------------------------------------------ Euclid gcd |
my $self = {}; |
|
|
sub euclid { |
$self->{NAV_HASH_FILE} = shift; |
my ($e,$f)=@_; |
$self->{PARM_HASH_FILE} = shift; |
my $a; my $b; my $r; |
$self->{GENERATE_COURSE_USER_OPT} = shift; |
if ($e>$f) { $b=$e; $r=$f; } else { $r=$e; $b=$f; } |
$self->{GENERATE_EMAIL_DISCUSS_STATUS} = shift; |
while ($r!=0) { |
|
$a=$b; $b=$r; |
# Resource cache stores navmap resources as we reference them. We generate |
$r=$a%$b; |
# them on-demand so we don't pay for creating resources unless we use them. |
} |
$self->{RESOURCE_CACHE} = {}; |
return $b; |
|
} |
# Network failure flag, if we accessed the course or user opt and |
|
# failed |
# ------------------------------------------------------------ Build page table |
$self->{NETWORK_FAILURE} = 0; |
|
|
sub tracetable { |
# tie the nav hash |
my ($sofar,$rid,$beenhere)=@_; |
|
my $further=$sofar; |
my %navmaphash; |
unless ($beenhere=~/\&$rid\&/) { |
my %parmhash; |
$beenhere.=$rid.'&'; |
if (!(tie(%navmaphash, 'GDBM_File', $self->{NAV_HASH_FILE}, |
|
&GDBM_READER(), 0640))) { |
if (defined($hash{'is_map_'.$rid})) { |
return undef; |
if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) && |
} |
(defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) { |
|
my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}}; |
if (!(tie(%parmhash, 'GDBM_File', $self->{PARM_HASH_FILE}, |
$sofar= |
&GDBM_READER(), 0640))) |
&tracetable($sofar,$hash{'map_start_'.$hash{'src_'.$rid}}, |
{ |
'&'.$frid.'&'); |
untie %{$self->{PARM_HASH}}; |
$sofar++; |
return undef; |
if ($hash{'src_'.$frid}) { |
} |
my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$frid}); |
|
if (($brepriv eq '2') || ($brepriv eq 'F')) { |
$self->{NAV_HASH} = \%navmaphash; |
if (defined($rows[$sofar])) { |
$self->{PARM_HASH} = \%parmhash; |
$rows[$sofar].='&'.$frid; |
$self->{INITED} = 0; |
} else { |
|
$rows[$sofar]=$frid; |
bless($self); |
} |
|
} |
return $self; |
} |
} |
} |
|
} else { |
sub init { |
$sofar++; |
my $self = shift; |
if ($hash{'src_'.$rid}) { |
if ($self->{INITED}) { return; } |
my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$rid}); |
|
if (($brepriv eq '2') || ($brepriv eq 'F')) { |
# If the course opt hash and the user opt hash should be generated, |
if (defined($rows[$sofar])) { |
# generate them |
$rows[$sofar].='&'.$rid; |
if ($self->{GENERATE_COURSE_USER_OPT}) { |
} else { |
my $uname=$ENV{'user.name'}; |
$rows[$sofar]=$rid; |
my $udom=$ENV{'user.domain'}; |
} |
my $uhome=$ENV{'user.home'}; |
} |
my $cid=$ENV{'request.course.id'}; |
} |
my $chome=$ENV{'course.'.$cid.'.home'}; |
} |
my ($cdom,$cnum)=split(/\_/,$cid); |
|
|
if (defined($hash{'to_'.$rid})) { |
my $userprefix=$uname.'_'.$udom.'_'; |
my $mincond=1; |
|
my $next=''; |
my %courserdatas; my %useropt; my %courseopt; my %userrdatas; |
map { |
unless ($uhome eq 'no_host') { |
my $thiscond= |
# ------------------------------------------------- Get coursedata (if present) |
&Apache::lonnet::directcondval($hash{'condid_'.$hash{'undercond_'.$_}}); |
unless ((time-$courserdatas{$cid.'.last_cache'})<240) { |
if ($thiscond>=$mincond) { |
my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum. |
if ($next) { |
':resourcedata',$chome); |
$next.=','.$_.':'.$thiscond; |
if ($reply!~/^error\:/) { |
} else { |
$courserdatas{$cid}=$reply; |
$next=$_.':'.$thiscond; |
$courserdatas{$cid.'.last_cache'}=time; |
} |
} |
if ($thiscond>$mincond) { $mincond=$thiscond; } |
# check to see if network failed |
} |
elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) |
} split(/\,/,$hash{'to_'.$rid}); |
{ |
map { |
$self->{NETWORK_FAILURE} = 1; |
my ($linkid,$condval)=split(/\:/,$_); |
} |
if ($condval>=$mincond) { |
} |
my $now=&tracetable($sofar,$hash{'goesto_'.$linkid},$beenhere); |
foreach (split(/\&/,$courserdatas{$cid})) { |
if ($now>$further) { $further=$now; } |
my ($name,$value)=split(/\=/,$_); |
} |
$courseopt{$userprefix.&Apache::lonnet::unescape($name)}= |
} split(/\,/,$next); |
&Apache::lonnet::unescape($value); |
|
} |
|
# --------------------------------------------------- Get userdata (if present) |
|
unless ((time-$userrdatas{$uname.'___'.$udom.'.last_cache'})<240) { |
|
my $reply=&Apache::lonnet::reply('dump:'.$udom.':'.$uname.':resourcedata',$uhome); |
|
if ($reply!~/^error\:/) { |
|
$userrdatas{$uname.'___'.$udom}=$reply; |
|
$userrdatas{$uname.'___'.$udom.'.last_cache'}=time; |
|
} |
|
# check to see if network failed |
|
elsif ( $reply=~/no.such.host/i || $reply=~/con.*lost/i ) |
|
{ |
|
$self->{NETWORK_FAILURE} = 1; |
|
} |
|
} |
|
foreach (split(/\&/,$userrdatas{$uname.'___'.$udom})) { |
|
my ($name,$value)=split(/\=/,$_); |
|
$useropt{$userprefix.&Apache::lonnet::unescape($name)}= |
|
&Apache::lonnet::unescape($value); |
|
} |
|
$self->{COURSE_OPT} = \%courseopt; |
|
$self->{USER_OPT} = \%useropt; |
|
} |
|
} |
|
|
|
if ($self->{GENERATE_EMAIL_DISCUSS_STATUS}) { |
|
my $cid=$ENV{'request.course.id'}; |
|
my ($cdom,$cnum)=split(/\_/,$cid); |
|
|
|
my %emailstatus = &Apache::lonnet::dump('email_status'); |
|
my $logoutTime = $emailstatus{'logout'}; |
|
my $courseLeaveTime = $emailstatus{'logout_'.$ENV{'request.course.id'}}; |
|
$self->{LAST_CHECK} = ($courseLeaveTime < $logoutTime ? |
|
$courseLeaveTime : $logoutTime); |
|
my %discussiontime = &Apache::lonnet::dump('discussiontimes', |
|
$cdom, $cnum); |
|
my %feedback=(); |
|
my %error=(); |
|
my $keys = &Apache::lonnet::reply('keys:'. |
|
$ENV{'user.domain'}.':'. |
|
$ENV{'user.name'}.':nohist_email', |
|
$ENV{'user.home'}); |
|
|
|
foreach my $msgid (split(/\&/, $keys)) { |
|
$msgid=&Apache::lonnet::unescape($msgid); |
|
my $plain=&Apache::lonnet::unescape(&Apache::lonnet::unescape($msgid)); |
|
if ($plain=~/(Error|Feedback) \[([^\]]+)\]/) { |
|
my ($what,$url)=($1,$2); |
|
my %status= |
|
&Apache::lonnet::get('email_status',[$msgid]); |
|
if ($status{$msgid}=~/^error\:/) { |
|
$status{$msgid}=''; |
|
} |
|
|
|
if (($status{$msgid} eq 'new') || |
|
(!$status{$msgid})) { |
|
if ($what eq 'Error') { |
|
$error{$url}.=','.$msgid; |
|
} else { |
|
$feedback{$url}.=','.$msgid; |
|
} |
|
} |
|
} |
|
} |
|
|
|
$self->{FEEDBACK} = \%feedback; |
|
$self->{ERROR_MSG} = \%error; # what is this? JB |
|
$self->{DISCUSSION_TIME} = \%discussiontime; |
|
$self->{EMAIL_STATUS} = \%emailstatus; |
|
|
|
} |
|
|
|
$self->{PARM_CACHE} = {}; |
|
$self->{INITED} = 1; |
|
} |
|
|
|
# Internal function: Takes a key to look up in the nav hash and implements internal |
|
# memory caching of that key. |
|
sub navhash { |
|
my $self = shift; my $key = shift; |
|
return $self->{NAV_HASH}->{$key}; |
|
} |
|
|
|
# Checks to see if coursemap is defined, matching test in old lonnavmaps |
|
sub courseMapDefined { |
|
my $self = shift; |
|
my $uri = &Apache::lonnet::clutter($ENV{'request.course.uri'}); |
|
|
|
my $firstres = $self->navhash("map_start_$uri"); |
|
my $lastres = $self->navhash("map_finish_$uri"); |
|
return $firstres && $lastres; |
|
} |
|
|
|
sub getIterator { |
|
my $self = shift; |
|
my $iterator = Apache::lonnavmaps::iterator->new($self, shift, shift, |
|
shift, undef, shift); |
|
return $iterator; |
|
} |
|
|
|
# unties the hash when done |
|
sub untieHashes { |
|
my $self = shift; |
|
untie %{$self->{NAV_HASH}}; |
|
untie %{$self->{PARM_HASH}}; |
|
} |
|
|
|
# Private method: Does the given resource (as a symb string) have |
|
# current discussion? Returns 0 if chat/mail data not extracted. |
|
sub hasDiscussion { |
|
my $self = shift; |
|
my $symb = shift; |
|
if (!defined($self->{DISCUSSION_TIME})) { return 0; } |
|
|
|
#return defined($self->{DISCUSSION_TIME}->{$symb}); |
|
return $self->{DISCUSSION_TIME}->{$symb} > |
|
$self->{LAST_CHECK}; |
|
} |
|
|
|
# Private method: Does the given resource (as a symb string) have |
|
# current feedback? Returns the string in the feedback hash, which |
|
# will be false if it does not exist. |
|
sub getFeedback { |
|
my $self = shift; |
|
my $symb = shift; |
|
|
|
if (!defined($self->{FEEDBACK})) { return ""; } |
|
|
|
return $self->{FEEDBACK}->{$symb}; |
|
} |
|
|
|
# Private method: Get the errors for that resource (by source). |
|
sub getErrors { |
|
my $self = shift; |
|
my $src = shift; |
|
|
|
if (!defined($self->{ERROR_MSG})) { return ""; } |
|
return $self->{ERROR_MSG}->{$src}; |
|
} |
|
|
|
=pod |
|
|
|
=item * B<getById>(id): |
|
|
} |
Based on the ID of the resource (1.1, 3.2, etc.), get a resource |
|
object for that resource. This method, or other methods that use it |
|
(as in the resource object) is the only proper way to obtain a |
|
resource object. |
|
|
|
=cut |
|
|
|
# The strategy here is to cache the resource objects, and only construct them |
|
# as we use them. The real point is to prevent reading any more from the tied |
|
# hash then we have to, which should hopefully alleviate speed problems. |
|
# Caching is just an incidental detail I throw in because it makes sense. |
|
|
|
sub getById { |
|
my $self = shift; |
|
my $id = shift; |
|
|
|
if (defined ($self->{RESOURCE_CACHE}->{$id})) |
|
{ |
|
return $self->{RESOURCE_CACHE}->{$id}; |
} |
} |
return $further; |
|
|
# resource handles inserting itself into cache. |
|
# Not clear why the quotes are necessary, but as of this |
|
# writing it doesn't work without them. |
|
return "Apache::lonnavmaps::resource"->new($self, $id); |
} |
} |
|
|
# ================================================================ Main Handler |
sub getBySymb { |
|
my $self = shift; |
|
my $symb = shift; |
|
my ($mapUrl, $id, $filename) = split (/___/, $symb); |
|
my $map = $self->getResourceByUrl($mapUrl); |
|
return $self->getById($map->map_pc() . '.' . $id); |
|
} |
|
|
sub handler { |
=pod |
my $r=shift; |
|
|
|
|
=item * B<firstResource>(): |
|
|
# ------------------------------------------- Set document type for header only |
Returns a resource object reference corresponding to the first |
|
resource in the navmap. |
|
|
if ($r->header_only) { |
=cut |
if ($ENV{'browser.mathml'}) { |
|
$r->content_type('text/xml'); |
|
} else { |
|
$r->content_type('text/html'); |
|
} |
|
$r->send_http_header; |
|
return OK; |
|
} |
|
|
|
my $requrl=$r->uri; |
|
# ----------------------------------------------------------------- Tie db file |
|
if ($ENV{'request.course.fn'}) { |
|
my $fn=$ENV{'request.course.fn'}; |
|
if (-e "$fn.db") { |
|
if (tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) { |
|
# ------------------------------------------------------------------- Hash tied |
|
my $firstres=$hash{'map_start_/res/'.$ENV{'request.course.uri'}}; |
|
my $lastres=$hash{'map_finish_/res/'.$ENV{'request.course.uri'}}; |
|
if (($firstres) && ($lastres)) { |
|
# ----------------------------------------------------------------- Render page |
|
|
|
@rows=(); |
|
|
|
&tracetable(0,$firstres,'&'.$lastres.'&'); |
|
if ($hash{'src_'.$lastres}) { |
|
my $brepriv= |
|
&Apache::lonnet::allowed('bre',$hash{'src_'.$lastres}); |
|
if (($brepriv eq '2') || ($brepriv eq 'F')) { |
|
$rows[$#rows+1]=''.$lastres; |
|
} |
|
} |
|
|
|
# ------------------------------------------------------------------ Page parms |
|
|
|
my $j; |
|
my $lcm=1; |
|
my $contents=0; |
|
|
|
# ---------------------------------------------- Go through table to get layout |
|
|
|
for ($i=0;$i<=$#rows;$i++) { |
|
if ($rows[$i]) { |
|
$contents++; |
|
my @colcont=split(/\&/,$rows[$i]); |
|
$lcm*=($#colcont+1)/euclid($lcm,($#colcont+1)); |
|
} |
|
} |
|
|
|
unless ($contents) { |
|
$r->content_type('text/html'); |
|
$r->send_http_header; |
|
$r->print('<html><body>Empty Map.</body></html>'); |
|
} else { |
|
# ------------------------------------------------------------------ Build page |
|
|
|
# ---------------------------------------------------------------- Send headers |
|
|
|
$r->content_type('text/html'); |
|
$r->send_http_header; |
|
$r->print( |
|
'<html><head><title>Navigate LON-CAPA Maps</title></head>'); |
|
|
|
$r->print('<body bgcolor="#FFFFFF">'. |
|
'<h1>Navigate Course Map</h1>'); |
|
$r->rflush(); |
|
# ----------------------------------------------------------------- Start table |
|
$r->print('<table cols="'.$lcm.'" border="0">'); |
|
for ($i=0;$i<=$#rows;$i++) { |
|
if ($rows[$i]) { |
|
$r->print("\n<tr>"); |
|
my @colcont=split(/\&/,$rows[$i]); |
|
my $avespan=$lcm/($#colcont+1); |
|
for ($j=0;$j<=$#colcont;$j++) { |
|
my $rid=$colcont[$j]; |
|
$r->print('<td><a href="'. |
|
$hash{'src_'.$rid}.'">'. |
|
$hash{'title_'.$rid}.'</a>'); |
|
$r->print('</td>'); |
|
} |
|
$r->print('</tr>'); |
|
} |
|
} |
|
$r->print("\n</table>"); |
|
|
|
$r->print('</body></html>'); |
|
# -------------------------------------------------------------------- End page |
|
} |
|
# ------------------------------------------------------------- End render page |
|
} else { |
|
$r->content_type('text/html'); |
|
$r->send_http_header; |
|
$r->print('<html><body>Coursemap undefined.</body></html>'); |
|
} |
|
# ------------------------------------------------------------------ Untie hash |
|
unless (untie(%hash)) { |
|
&Apache::lonnet::logthis("<font color=blue>WARNING: ". |
|
"Could not untie coursemap $fn (browse).</font>"); |
|
} |
|
# -------------------------------------------------------------------- All done |
|
return OK; |
|
# ----------------------------------------------- Errors, hash could no be tied |
|
} |
|
} |
|
} |
|
|
|
$ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; |
sub firstResource { |
return HTTP_NOT_ACCEPTABLE; |
my $self = shift; |
|
my $firstResource = $self->navhash('map_start_' . |
|
&Apache::lonnet::clutter($ENV{'request.course.uri'})); |
|
return $self->getById($firstResource); |
|
} |
|
|
|
=pod |
|
|
|
=item * B<finishResource>(): |
|
|
|
Returns a resource object reference corresponding to the last resource |
|
in the navmap. |
|
|
|
=cut |
|
|
|
sub finishResource { |
|
my $self = shift; |
|
my $firstResource = $self->navhash('map_finish_' . |
|
&Apache::lonnet::clutter($ENV{'request.course.uri'})); |
|
return $self->getById($firstResource); |
|
} |
|
|
|
# Parmval reads the parm hash and cascades the lookups. parmval_real does |
|
# the actual lookup; parmval caches the results. |
|
sub parmval { |
|
my $self = shift; |
|
my ($what,$symb)=@_; |
|
my $hashkey = $what."|||".$symb; |
|
|
|
if (defined($self->{PARM_CACHE}->{$hashkey})) { |
|
return $self->{PARM_CACHE}->{$hashkey}; |
|
} |
|
|
|
my $result = $self->parmval_real($what, $symb); |
|
$self->{PARM_CACHE}->{$hashkey} = $result; |
|
return $result; |
|
} |
|
|
|
sub parmval_real { |
|
my $self = shift; |
|
my ($what,$symb) = @_; |
|
|
|
my $cid=$ENV{'request.course.id'}; |
|
my $csec=$ENV{'request.course.sec'}; |
|
my $uname=$ENV{'user.name'}; |
|
my $udom=$ENV{'user.domain'}; |
|
|
|
unless ($symb) { return ''; } |
|
my $result=''; |
|
|
|
my ($mapname,$id,$fn)=split(/\_\_\_/,$symb); |
|
|
|
# ----------------------------------------------------- Cascading lookup scheme |
|
my $rwhat=$what; |
|
$what=~s/^parameter\_//; |
|
$what=~s/\_/\./; |
|
|
|
my $symbparm=$symb.'.'.$what; |
|
my $mapparm=$mapname.'___(all).'.$what; |
|
my $usercourseprefix=$uname.'_'.$udom.'_'.$cid; |
|
|
|
my $seclevel= $usercourseprefix.'.['.$csec.'].'.$what; |
|
my $seclevelr=$usercourseprefix.'.['.$csec.'].'.$symbparm; |
|
my $seclevelm=$usercourseprefix.'.['.$csec.'].'.$mapparm; |
|
|
|
my $courselevel= $usercourseprefix.'.'.$what; |
|
my $courselevelr=$usercourseprefix.'.'.$symbparm; |
|
my $courselevelm=$usercourseprefix.'.'.$mapparm; |
|
|
|
my $useropt = $self->{USER_OPT}; |
|
my $courseopt = $self->{COURSE_OPT}; |
|
my $parmhash = $self->{PARM_HASH}; |
|
|
|
# ---------------------------------------------------------- first, check user |
|
if ($uname and defined($useropt)) { |
|
if (defined($$useropt{$courselevelr})) { return $$useropt{$courselevelr}; } |
|
if (defined($$useropt{$courselevelm})) { return $$useropt{$courselevelm}; } |
|
if (defined($$useropt{$courselevel})) { return $$useropt{$courselevel}; } |
|
} |
|
|
|
# ------------------------------------------------------- second, check course |
|
if ($csec and defined($courseopt)) { |
|
if (defined($$courseopt{$seclevelr})) { return $$courseopt{$seclevelr}; } |
|
if (defined($$courseopt{$seclevelm})) { return $$courseopt{$seclevelm}; } |
|
if (defined($$courseopt{$seclevel})) { return $$courseopt{$seclevel}; } |
|
} |
|
|
|
if (defined($courseopt)) { |
|
if (defined($$courseopt{$courselevelr})) { return $$courseopt{$courselevelr}; } |
|
if (defined($$courseopt{$courselevelm})) { return $$courseopt{$courselevelm}; } |
|
if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } |
|
} |
|
|
|
# ----------------------------------------------------- third, check map parms |
|
|
|
my $thisparm=$$parmhash{$symbparm}; |
|
if (defined($thisparm)) { return $thisparm; } |
|
|
|
# ----------------------------------------------------- fourth , check default |
|
|
|
my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default'); |
|
if (defined($default)) { return $default} |
|
|
|
# --------------------------------------------------- fifth , cascade up parts |
|
|
|
my ($space,@qualifier)=split(/\./,$rwhat); |
|
my $qualifier=join('.',@qualifier); |
|
unless ($space eq '0') { |
|
my @parts=split(/_/,$space); |
|
my $id=pop(@parts); |
|
my $part=join('_',@parts); |
|
if ($part eq '') { $part='0'; } |
|
my $partgeneral=$self->parmval($part.".$qualifier",$symb); |
|
if (defined($partgeneral)) { return $partgeneral; } |
|
} |
|
return ''; |
|
} |
|
|
|
=pod |
|
|
|
=item * B<getResourceByUrl>(url): |
|
|
|
Retrieves a resource object by URL of the resource. If passed a |
|
resource object, it will simply return it, so it is safe to use this |
|
method in code like "$res = $navmap->getResourceByUrl($res)", if |
|
you're not sure if $res is already an object, or just a URL. If the |
|
resource appears multiple times in the course, only the first instance |
|
will be returned. As a result, this is probably useful only for maps. |
|
|
|
=item * B<retrieveResources>(map, filterFunc, recursive, bailout): |
|
|
|
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 |
|
function that takes a resource object as its one argument and returns |
|
true if the resource should be included, or false if it should not |
|
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. 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. The |
|
resources will be returned in a list containing the resource objects |
|
for the corresponding resources, with B<no structure information> 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 |
|
many resources match a given description; for this use, if all you |
|
want to know is if I<any> resources match the description, the bailout |
|
parameter will allow you to avoid potentially expensive enumeration of |
|
all matching resources. |
|
|
|
=item * B<hasResources>(map, filterFunc, recursive): |
|
|
|
Convience method for |
|
|
|
scalar(retrieveResources($map, $filterFunc, $recursive, 1)) > 0 |
|
|
|
which will tell whether the map has resources matching the description |
|
in the filter function. |
|
|
|
=cut |
|
|
|
sub getResourceByUrl { |
|
my $self = shift; |
|
my $resUrl = shift; |
|
|
|
if (ref($resUrl)) { return $resUrl; } |
|
|
|
$resUrl = &Apache::lonnet::clutter($resUrl); |
|
my $resId = $self->{NAV_HASH}->{'ids_' . $resUrl}; |
|
if ($resId =~ /,/) { |
|
$resId = (split (/,/, $resId))[0]; |
|
} |
|
if (!$resId) { return ''; } |
|
return $self->getById($resId); |
|
} |
|
|
|
sub retrieveResources { |
|
my $self = shift; |
|
my $map = shift; |
|
my $filterFunc = shift; |
|
if (!defined ($filterFunc)) { |
|
$filterFunc = sub {return 1;}; |
|
} |
|
my $recursive = shift; |
|
if (!defined($recursive)) { $recursive = 1; } |
|
my $bailout = shift; |
|
if (!defined($bailout)) { $bailout = 0; } |
|
|
|
# Create the necessary iterator. |
|
if (!ref($map)) { # assume it's a url of a map. |
|
$map = $self->getResourceByUrl($map); |
|
} |
|
|
|
# Check the map's validity. |
|
if (!$map || !$map->is_map()) { |
|
# Oh, to throw an exception.... how I'd love that! |
|
return (); |
|
} |
|
|
|
# Get an iterator. |
|
my $it = $self->getIterator($map->map_start(), $map->map_finish(), |
|
!$recursive); |
|
|
|
my @resources = (); |
|
|
|
# Run down the iterator and collect the resources. |
|
my $depth = 1; |
|
$it->next(); |
|
my $curRes = $it->next(); |
|
|
|
while ($depth > 0) { |
|
if ($curRes == $it->BEGIN_MAP()) { |
|
$depth++; |
|
} |
|
if ($curRes == $it->END_MAP()) { |
|
$depth--; |
|
} |
|
|
|
if (ref($curRes)) { |
|
if (!&$filterFunc($curRes)) { |
|
next; |
|
} |
|
|
|
push @resources, $curRes; |
|
|
|
if ($bailout) { |
|
return @resources; |
|
} |
|
} |
|
|
|
$curRes = $it->next(); |
|
} |
|
|
|
return @resources; |
|
} |
|
|
|
sub hasResource { |
|
my $self = shift; |
|
my $map = shift; |
|
my $filterFunc = shift; |
|
my $recursive = shift; |
|
|
|
return scalar($self->retrieveResources($map, $filterFunc, $recursive, 1)) > 0; |
} |
} |
|
|
1; |
1; |
__END__ |
|
|
|
|
package Apache::lonnavmaps::iterator; |
|
|
|
=pod |
|
|
|
=back |
|
|
|
=head1 Object: navmap Iterator |
|
|
|
An I<iterator> encapsulates the logic required to traverse a data |
|
structure. navmap uses an iterator to traverse the course map |
|
according to the criteria you wish to use. |
|
|
|
To obtain an iterator, call the B<getIterator>() function of a |
|
B<navmap> object. (Do not instantiate Apache::lonnavmaps::iterator |
|
directly.) This will return a reference to the iterator: |
|
|
|
C<my $resourceIterator = $navmap-E<gt>getIterator();> |
|
|
|
To get the next thing from the iterator, call B<next>: |
|
|
|
C<my $nextThing = $resourceIterator-E<gt>next()> |
|
|
|
getIterator behaves as follows: |
|
|
|
=over 4 |
|
|
|
=item * B<getIterator>(firstResource, finishResource, filterHash, condition, forceTop, returnTopMap): |
|
|
|
All parameters are optional. firstResource is a resource reference |
|
corresponding to where the iterator should start. It defaults to |
|
navmap->firstResource() for the corresponding nav map. finishResource |
|
corresponds to where you want the iterator to end, defaulting to |
|
navmap->finishResource(). filterHash is a hash used as a set |
|
containing strings representing the resource IDs, defaulting to |
|
empty. Condition is a 1 or 0 that sets what to do with the filter |
|
hash: If a 0, then only resource that exist IN the filterHash will be |
|
recursed on. If it is a 1, only resources NOT in the filterHash will |
|
be recursed on. Defaults to 0. forceTop is a boolean value. If it is |
|
false (default), the iterator will only return the first level of map |
|
that is not just a single, 'redirecting' map. If true, the iterator |
|
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. |
|
|
|
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 |
|
shown. Changing the condition to 1 and including some values in the |
|
hash will allow you to selectively suppress parts of the navmap, while |
|
leaving it on 0 and adding things to the hash will allow you to |
|
selectively add parts of the nav map. See the handler code for |
|
examples. |
|
|
|
The iterator will return either a reference to a resource object, or a |
|
token representing something in the map, such as the beginning of a |
|
new branch. The possible tokens are: |
|
|
|
=over 4 |
|
|
|
=item * BEGIN_MAP: |
|
|
|
A new map is being recursed into. This is returned I<after> the map |
|
resource itself is returned. |
|
|
|
=item * END_MAP: |
|
|
|
The map is now done. |
|
|
|
=item * BEGIN_BRANCH: |
|
|
|
A branch is now starting. The next resource returned will be the first |
|
in that branch. |
|
|
|
=item * END_BRANCH: |
|
|
|
The branch is now done. |
|
|
|
=back |
|
|
|
The tokens are retreivable via methods on the iterator object, i.e., |
|
$iterator->END_MAP. |
|
|
|
Maps can contain empty resources. The iterator will automatically skip |
|
over such resources, but will still treat the structure |
|
correctly. Thus, a complicated map with several branches, but |
|
consisting entirely of empty resources except for one beginning or |
|
ending resource, will cause a lot of BRANCH_STARTs and BRANCH_ENDs, |
|
but only one resource will be returned. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
# Here are the tokens for the iterator: |
|
|
|
sub BEGIN_MAP { return 1; } # begining of a new map |
|
sub END_MAP { return 2; } # end of the map |
|
sub BEGIN_BRANCH { return 3; } # beginning of a branch |
|
sub END_BRANCH { return 4; } # end of a branch |
|
sub FORWARD { return 1; } # go forward |
|
sub BACKWARD { return 2; } |
|
|
|
sub min { |
|
(my $a, my $b) = @_; |
|
if ($a < $b) { return $a; } else { return $b; } |
|
} |
|
|
|
# In the CVS repository, documentation of this algorithm is included |
|
# in /doc/lonnavdocs, as a PDF and .tex source. Markers like **1** |
|
# will reference the same location in the text as the part of the |
|
# algorithm is running through. |
|
|
|
sub new { |
|
# magic invocation to create a class instance |
|
my $proto = shift; |
|
my $class = ref($proto) || $proto; |
|
my $self = {}; |
|
|
|
$self->{NAV_MAP} = shift; |
|
return undef unless ($self->{NAV_MAP}); |
|
|
|
# Handle the parameters |
|
$self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); |
|
$self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); |
|
|
|
# If the given resources are just the ID of the resource, get the |
|
# objects |
|
if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = |
|
$self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); } |
|
if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = |
|
$self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); } |
|
|
|
$self->{FILTER} = shift; |
|
|
|
# A hash, used as a set, of resource already seen |
|
$self->{ALREADY_SEEN} = shift; |
|
if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; |
|
$self->{CONDITION} = shift; |
|
|
|
# Do we want to automatically follow "redirection" maps? |
|
$self->{FORCE_TOP} = shift; |
|
|
|
# Do we want to return the top-level map object (resource 0.0)? |
|
$self->{RETURN_0} = shift; |
|
# have we done that yet? |
|
$self->{HAVE_RETURNED_0} = 0; |
|
|
|
# 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. |
|
|
|
# The processing steps are exactly the same, except for a few small |
|
# changes, so I bundle those up in the following list of two elements: |
|
# (direction_to_iterate, VAL_name, next_resource_method_to_call, |
|
# first_resource). |
|
# This prevents writing nearly-identical code twice. |
|
my @iterations = ( [FORWARD(), 'TOP_DOWN_VAL', 'getNext', |
|
'FIRST_RESOURCE'], |
|
[BACKWARD(), 'BOT_UP_VAL', 'getPrevious', |
|
'FINISH_RESOURCE'] ); |
|
|
|
my $maxDepth = 0; # tracks max depth |
|
|
|
# If there is only one resource in this map, and it's a map, we |
|
# want to remember that, so the user can ask for the first map |
|
# that isn't just a redirector. |
|
my $resource; my $resourceCount = 0; |
|
|
|
# **1** |
|
|
|
foreach my $pass (@iterations) { |
|
my $direction = $pass->[0]; |
|
my $valName = $pass->[1]; |
|
my $nextResourceMethod = $pass->[2]; |
|
my $firstResourceName = $pass->[3]; |
|
|
|
my $iterator = Apache::lonnavmaps::DFSiterator->new($self->{NAV_MAP}, |
|
$self->{FIRST_RESOURCE}, |
|
$self->{FINISH_RESOURCE}, |
|
{}, undef, 0, $direction); |
|
|
|
# prime the recursion |
|
$self->{$firstResourceName}->{DATA}->{$valName} = 0; |
|
my $depth = 0; |
|
$iterator->next(); |
|
my $curRes = $iterator->next(); |
|
while ($depth > -1) { |
|
if ($curRes == $iterator->BEGIN_MAP()) { $depth++; } |
|
if ($curRes == $iterator->END_MAP()) { $depth--; } |
|
|
|
if (ref($curRes)) { |
|
# If there's only one resource, this will save it |
|
# we have to filter empty resources from consideration here, |
|
# or even "empty", redirecting maps have two (start & finish) |
|
# or three (start, finish, plus redirector) |
|
if($direction == FORWARD && $curRes->src()) { |
|
$resource = $curRes; $resourceCount++; |
|
} |
|
my $resultingVal = $curRes->{DATA}->{$valName}; |
|
my $nextResources = $curRes->$nextResourceMethod(); |
|
my $nextCount = scalar(@{$nextResources}); |
|
|
|
if ($nextCount == 1) { # **3** |
|
my $current = $nextResources->[0]->{DATA}->{$valName} || 999999999; |
|
$nextResources->[0]->{DATA}->{$valName} = min($resultingVal, $current); |
|
} |
|
|
|
if ($nextCount > 1) { # **4** |
|
foreach my $res (@{$nextResources}) { |
|
my $current = $res->{DATA}->{$valName} || 999999999; |
|
$res->{DATA}->{$valName} = min($current, $resultingVal + 1); |
|
} |
|
} |
|
} |
|
|
|
# Assign the final val (**2**) |
|
if (ref($curRes) && $direction == BACKWARD()) { |
|
my $finalDepth = min($curRes->{DATA}->{TOP_DOWN_VAL}, |
|
$curRes->{DATA}->{BOT_UP_VAL}); |
|
|
|
$curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth; |
|
if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;} |
|
} |
|
$curRes = $iterator->next(); |
|
} |
|
} |
|
|
|
# Check: Was this only one resource, a map? |
|
if ($resourceCount == 1 && $resource->is_map() && !$self->{FORCE_TOP}) { |
|
my $firstResource = $resource->map_start(); |
|
my $finishResource = $resource->map_finish(); |
|
return |
|
Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, |
|
$finishResource, $self->{FILTER}, |
|
$self->{ALREADY_SEEN}, |
|
$self->{CONDITION}, 0); |
|
|
|
} |
|
|
|
# Set up some bookkeeping information. |
|
$self->{CURRENT_DEPTH} = 0; |
|
$self->{MAX_DEPTH} = $maxDepth; |
|
$self->{STACK} = []; |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 0; |
|
|
|
for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) { |
|
push @{$self->{STACK}}, []; |
|
} |
|
|
|
# Prime the recursion w/ the first resource **5** |
|
push @{$self->{STACK}->[0]}, $self->{FIRST_RESOURCE}; |
|
$self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; |
|
|
|
bless ($self); |
|
|
|
return $self; |
|
} |
|
|
|
sub next { |
|
my $self = shift; |
|
|
|
# If we want to return the top-level map object, and haven't yet, |
|
# do so. |
|
if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) { |
|
$self->{HAVE_RETURNED_0} = 1; |
|
return $self->{NAV_MAP}->getById('0.0'); |
|
} |
|
|
|
if ($self->{RECURSIVE_ITERATOR_FLAG}) { |
|
# grab the next from the recursive iterator |
|
my $next = $self->{RECURSIVE_ITERATOR}->next(); |
|
|
|
# is it a begin or end map? If so, update the depth |
|
if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; } |
|
if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; } |
|
|
|
# Are we back at depth 0? If so, stop recursing |
|
if ($self->{RECURSIVE_DEPTH} == 0) { |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 0; |
|
} |
|
|
|
return $next; |
|
} |
|
|
|
if (defined($self->{FORCE_NEXT})) { |
|
my $tmp = $self->{FORCE_NEXT}; |
|
$self->{FORCE_NEXT} = undef; |
|
return $tmp; |
|
} |
|
|
|
# Have we not yet begun? If not, return BEGIN_MAP and |
|
# remember we've started. |
|
if ( !$self->{STARTED} ) { |
|
$self->{STARTED} = 1; |
|
return $self->BEGIN_MAP(); |
|
} |
|
|
|
# Here's the guts of the iterator. |
|
|
|
# Find the next resource, if any. |
|
my $found = 0; |
|
my $i = $self->{MAX_DEPTH}; |
|
my $newDepth; |
|
my $here; |
|
while ( $i >= 0 && !$found ) { |
|
if ( scalar(@{$self->{STACK}->[$i]}) > 0 ) { # **6** |
|
$here = pop @{$self->{STACK}->[$i]}; # **7** |
|
$found = 1; |
|
$newDepth = $i; |
|
} |
|
$i--; |
|
} |
|
|
|
# If we still didn't find anything, we're done. |
|
if ( !$found ) { |
|
# We need to get back down to the correct branch depth |
|
if ( $self->{CURRENT_DEPTH} > 0 ) { |
|
$self->{CURRENT_DEPTH}--; |
|
return END_BRANCH(); |
|
} else { |
|
return END_MAP(); |
|
} |
|
} |
|
|
|
# If this is not a resource, it must be an END_BRANCH marker we want |
|
# to return directly. |
|
if (!ref($here)) { # **8** |
|
if ($here == END_BRANCH()) { # paranoia, in case of later extension |
|
$self->{CURRENT_DEPTH}--; |
|
return $here; |
|
} |
|
} |
|
|
|
# Otherwise, it is a resource and it's safe to store in $self->{HERE} |
|
$self->{HERE} = $here; |
|
|
|
# Get to the right level |
|
if ( $self->{CURRENT_DEPTH} > $newDepth ) { |
|
push @{$self->{STACK}->[$newDepth]}, $here; |
|
$self->{CURRENT_DEPTH}--; |
|
return END_BRANCH(); |
|
} |
|
if ( $self->{CURRENT_DEPTH} < $newDepth) { |
|
push @{$self->{STACK}->[$newDepth]}, $here; |
|
$self->{CURRENT_DEPTH}++; |
|
return BEGIN_BRANCH(); |
|
} |
|
|
|
# If we made it here, we have the next resource, and we're at the |
|
# right branch level. So let's examine the resource for where |
|
# we can get to from here. |
|
|
|
# So we need to look at all the resources we can get to from here, |
|
# categorize them if we haven't seen them, remember if we have a new |
|
my $nextUnfiltered = $here->getNext(); |
|
my $maxDepthAdded = -1; |
|
|
|
for (@$nextUnfiltered) { |
|
if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { |
|
my $depth = $_->{DATA}->{DISPLAY_DEPTH}; |
|
push @{$self->{STACK}->[$depth]}, $_; |
|
$self->{ALREADY_SEEN}->{$_->{ID}} = 1; |
|
if ($maxDepthAdded < $depth) { $maxDepthAdded = $depth; } |
|
} |
|
} |
|
|
|
# Is this the end of a branch? If so, all of the resources examined above |
|
# led to lower levels then the one we are currently at, so we push a END_BRANCH |
|
# marker onto the stack so we don't forget. |
|
# Example: For the usual A(BC)(DE)F case, when the iterator goes down the |
|
# BC branch and gets to C, it will see F as the only next resource, but it's |
|
# one level lower. Thus, this is the end of the branch, since there are no |
|
# more resources added to this level or above. |
|
# We don't do this if the examined resource is the finish resource, |
|
# because the condition given above is true, but the "END_MAP" will |
|
# take care of things and we should already be at depth 0. |
|
my $isEndOfBranch = $maxDepthAdded < $self->{CURRENT_DEPTH}; |
|
if ($isEndOfBranch && $here != $self->{FINISH_RESOURCE}) { # **9** |
|
push @{$self->{STACK}->[$self->{CURRENT_DEPTH}]}, END_BRANCH(); |
|
} |
|
|
|
# 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_map() && |
|
(defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 1; |
|
my $firstResource = $self->{HERE}->map_start(); |
|
my $finishResource = $self->{HERE}->map_finish(); |
|
|
|
$self->{RECURSIVE_ITERATOR} = |
|
Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, |
|
$finishResource, $self->{FILTER}, |
|
$self->{ALREADY_SEEN}, $self->{CONDITION}); |
|
} |
|
|
|
# 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(); |
|
if (!$self->{HERE}->src() || |
|
(!($browsePriv eq 'F') && !($browsePriv eq '2')) ) { |
|
return $self->next(); |
|
} |
|
|
|
return $self->{HERE}; |
|
|
|
} |
|
|
|
=pod |
|
|
|
The other method available on the iterator is B<getStack>, which |
|
returns an array populated with the current 'stack' of maps, as |
|
references to the resource objects. Example: This is useful when |
|
making the navigation map, as we need to check whether we are under a |
|
page map to see if we need to link directly to the resource, or to the |
|
page. The first elements in the array will correspond to the top of |
|
the stack (most inclusive map). |
|
|
|
=cut |
|
|
|
sub getStack { |
|
my $self=shift; |
|
|
|
my @stack; |
|
|
|
$self->populateStack(\@stack); |
|
|
|
return \@stack; |
|
} |
|
|
|
# Private method: Calls the iterators recursively to populate the stack. |
|
sub populateStack { |
|
my $self=shift; |
|
my $stack = shift; |
|
|
|
push @$stack, $self->{HERE} if ($self->{HERE}); |
|
|
|
if ($self->{RECURSIVE_ITERATOR_FLAG}) { |
|
$self->{RECURSIVE_ITERATOR}->populateStack($stack); |
|
} |
|
} |
|
|
|
1; |
|
|
|
package Apache::lonnavmaps::DFSiterator; |
|
|
|
# Not documented in the perldoc: This is a simple iterator that just walks |
|
# through the nav map and presents the resources in a depth-first search |
|
# fashion, ignorant of conditionals, randomized resources, etc. It presents |
|
# BEGIN_MAP and END_MAP, but does not understand branches at all. It is |
|
# useful for pre-processing of some kind, and is in fact used by the main |
|
# iterator that way, but that's about it. |
|
# One could imagine merging this into the init routine of the main iterator, |
|
# but this might as well be left seperate, since it is possible some other |
|
# use might be found for it. - Jeremy |
|
|
|
# Unlike the main iterator, this DOES return all resources, even blank ones. |
|
# The main iterator needs them to correctly preprocess the map. |
|
|
|
sub BEGIN_MAP { return 1; } # begining of a new map |
|
sub END_MAP { return 2; } # end of the map |
|
sub FORWARD { return 1; } # go forward |
|
sub BACKWARD { return 2; } |
|
|
|
# Params: Nav map ref, first resource id/ref, finish resource id/ref, |
|
# filter hash ref (or undef), already seen hash or undef, condition |
|
# (as in main iterator), direction FORWARD or BACKWARD (undef->forward). |
|
sub new { |
|
# magic invocation to create a class instance |
|
my $proto = shift; |
|
my $class = ref($proto) || $proto; |
|
my $self = {}; |
|
|
|
$self->{NAV_MAP} = shift; |
|
return undef unless ($self->{NAV_MAP}); |
|
|
|
$self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); |
|
$self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); |
|
|
|
# If the given resources are just the ID of the resource, get the |
|
# objects |
|
if (!ref($self->{FIRST_RESOURCE})) { $self->{FIRST_RESOURCE} = |
|
$self->{NAV_MAP}->getById($self->{FIRST_RESOURCE}); } |
|
if (!ref($self->{FINISH_RESOURCE})) { $self->{FINISH_RESOURCE} = |
|
$self->{NAV_MAP}->getById($self->{FINISH_RESOURCE}); } |
|
|
|
$self->{FILTER} = shift; |
|
|
|
# A hash, used as a set, of resource already seen |
|
$self->{ALREADY_SEEN} = shift; |
|
if (!defined($self->{ALREADY_SEEN})) { $self->{ALREADY_SEEN} = {} }; |
|
$self->{CONDITION} = shift; |
|
$self->{DIRECTION} = shift || FORWARD(); |
|
|
|
# Flag: Have we started yet? |
|
$self->{STARTED} = 0; |
|
|
|
# Should we continue calling the recursive iterator, if any? |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 0; |
|
# The recursive iterator, if any |
|
$self->{RECURSIVE_ITERATOR} = undef; |
|
# Are we recursing on a map, or a branch? |
|
$self->{RECURSIVE_MAP} = 1; # we'll manually unset this when recursing on branches |
|
# And the count of how deep it is, so that this iterator can keep track of |
|
# when to pick back up again. |
|
$self->{RECURSIVE_DEPTH} = 0; |
|
|
|
# For keeping track of our branches, we maintain our own stack |
|
$self->{STACK} = []; |
|
|
|
# Start with the first resource |
|
if ($self->{DIRECTION} == FORWARD) { |
|
push @{$self->{STACK}}, $self->{FIRST_RESOURCE}; |
|
} else { |
|
push @{$self->{STACK}}, $self->{FINISH_RESOURCE}; |
|
} |
|
|
|
bless($self); |
|
return $self; |
|
} |
|
|
|
sub next { |
|
my $self = shift; |
|
|
|
# Are we using a recursive iterator? If so, pull from that and |
|
# watch the depth; we want to resume our level at the correct time. |
|
if ($self->{RECURSIVE_ITERATOR_FLAG}) { |
|
# grab the next from the recursive iterator |
|
my $next = $self->{RECURSIVE_ITERATOR}->next(); |
|
|
|
# is it a begin or end map? Update depth if so |
|
if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; } |
|
if ($next == END_MAP() ) { $self->{RECURSIVE_DEPTH}--; } |
|
|
|
# Are we back at depth 0? If so, stop recursing. |
|
if ($self->{RECURSIVE_DEPTH} == 0) { |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 0; |
|
} |
|
|
|
return $next; |
|
} |
|
|
|
# Is there a current resource to grab? If not, then return |
|
# END_MAP, which will end the iterator. |
|
if (scalar(@{$self->{STACK}}) == 0) { |
|
return $self->END_MAP(); |
|
} |
|
|
|
# Have we not yet begun? If not, return BEGIN_MAP and |
|
# remember that we've started. |
|
if ( !$self->{STARTED} ) { |
|
$self->{STARTED} = 1; |
|
return $self->BEGIN_MAP; |
|
} |
|
|
|
# Get the next resource in the branch |
|
$self->{HERE} = pop @{$self->{STACK}}; |
|
|
|
# remember that we've seen this, so we don't return it again later |
|
$self->{ALREADY_SEEN}->{$self->{HERE}->{ID}} = 1; |
|
|
|
# Get the next possible resources |
|
my $nextUnfiltered; |
|
if ($self->{DIRECTION} == FORWARD()) { |
|
$nextUnfiltered = $self->{HERE}->getNext(); |
|
} else { |
|
$nextUnfiltered = $self->{HERE}->getPrevious(); |
|
} |
|
my $next = []; |
|
|
|
# filter the next possibilities to remove things we've |
|
# already seen. |
|
foreach (@$nextUnfiltered) { |
|
if (!defined($self->{ALREADY_SEEN}->{$_->{ID}})) { |
|
push @$next, $_; |
|
} |
|
} |
|
|
|
while (@$next) { |
|
# copy the next possibilities over to the stack |
|
push @{$self->{STACK}}, shift @$next; |
|
} |
|
|
|
# If this is a map and we want to recurse down it... (not filtered out) |
|
if ($self->{HERE}->is_map() && |
|
(defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) { |
|
$self->{RECURSIVE_ITERATOR_FLAG} = 1; |
|
my $firstResource = $self->{HERE}->map_start(); |
|
my $finishResource = $self->{HERE}->map_finish(); |
|
|
|
$self->{RECURSIVE_ITERATOR} = |
|
Apache::lonnavmaps::DFSiterator->new ($self->{NAV_MAP}, $firstResource, |
|
$finishResource, $self->{FILTER}, $self->{ALREADY_SEEN}, |
|
$self->{CONDITION}, $self->{DIRECTION}); |
|
} |
|
|
|
return $self->{HERE}; |
|
} |
|
|
|
1; |
|
|
|
package Apache::lonnavmaps::resource; |
|
|
|
use Apache::lonnet; |
|
|
|
=pod |
|
|
|
=head1 Object: resource |
|
|
|
A resource object encapsulates a resource in a resource map, allowing |
|
easy manipulation of the resource, querying the properties of the |
|
resource (including user properties), and represents a reference that |
|
can be used as the canonical representation of the resource by |
|
lonnavmap clients like renderers. |
|
|
|
A resource only makes sense in the context of a navmap, as some of the |
|
data is stored in the navmap object. |
|
|
|
You will probably never need to instantiate this object directly. Use |
|
Apache::lonnavmaps::navmap, and use the "start" method to obtain the |
|
starting resource. |
|
|
|
=head2 Public Members |
|
|
|
resource objects have a hash called DATA ($resourceRef->{DATA}) that |
|
you can store whatever you want in. This allows you to easily do |
|
two-pass algorithms without worrying about managing your own |
|
resource->data hash. |
|
|
|
=head2 Methods |
|
|
|
=over 4 |
|
|
|
=item * B<new>($navmapRef, $idString): |
|
|
|
The first arg is a reference to the parent navmap object. The second |
|
is the idString of the resource itself. Very rarely, if ever, called |
|
directly. Use the nav map->getByID() method. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub new { |
|
# magic invocation to create a class instance |
|
my $proto = shift; |
|
my $class = ref($proto) || $proto; |
|
my $self = {}; |
|
|
|
$self->{NAV_MAP} = shift; |
|
$self->{ID} = shift; |
|
|
|
# Store this new resource in the parent nav map's cache. |
|
$self->{NAV_MAP}->{RESOURCE_CACHE}->{$self->{ID}} = $self; |
|
$self->{RESOURCE_ERROR} = 0; |
|
|
|
# A hash that can be used by two-pass algorithms to store data |
|
# about this resource in. Not used by the resource object |
|
# directly. |
|
$self->{DATA} = {}; |
|
|
|
bless($self); |
|
|
|
return $self; |
|
} |
|
|
|
# private function: simplify the NAV_HASH lookups we keep doing |
|
# pass the name, and to automatically append my ID, pass a true val on the |
|
# second param |
|
sub navHash { |
|
my $self = shift; |
|
my $param = shift; |
|
my $id = shift; |
|
return $self->{NAV_MAP}->navhash($param . ($id?$self->{ID}:"")); |
|
} |
|
|
|
=pod |
|
|
|
B<Metadata Retreival> |
|
|
|
These are methods that help you retrieve metadata about the resource: |
|
Method names are based on the fields in the compiled course |
|
representation. |
|
|
|
=over 4 |
|
|
|
=item * B<compTitle>: |
|
|
|
Returns a "composite title", that is equal to $res->title() if the |
|
resource has a title, and is otherwise the last part of the URL (e.g., |
|
"problem.problem"). |
|
|
|
=item * B<ext>: |
|
|
|
Returns true if the resource is external. |
|
|
|
=item * B<goesto>: |
|
|
|
Returns the "goesto" value from the compiled nav map. (It is likely |
|
you want to use B<getNext> instead.) |
|
|
|
=item * B<kind>: |
|
|
|
Returns the kind of the resource from the compiled nav map. |
|
|
|
=item * B<randomout>: |
|
|
|
Returns true if this resource was chosen to NOT be shown to the user |
|
by the random map selection feature. In other words, this is usually |
|
false. |
|
|
|
=item * B<randompick>: |
|
|
|
Returns true for a map if the randompick feature is being used on the |
|
map. (?) |
|
|
|
=item * B<src>: |
|
|
|
Returns the source for the resource. |
|
|
|
=item * B<symb>: |
|
|
|
Returns the symb for the resource. |
|
|
|
=item * B<title>: |
|
|
|
Returns the title of the resource. |
|
|
|
=item * B<to>: |
|
|
|
Returns the "to" value from the compiled nav map. (It is likely you |
|
want to use B<getNext> instead.) |
|
|
|
=back |
|
|
|
=cut |
|
|
|
# These info functions can be used directly, as they don't return |
|
# resource information. |
|
sub comesfrom { my $self=shift; return $self->navHash("comesfrom_", 1); } |
|
sub ext { my $self=shift; return $self->navHash("ext_", 1) eq 'true:'; } |
|
sub from { my $self=shift; return $self->navHash("from_", 1); } |
|
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 randompick { |
|
my $self = shift; |
|
return $self->{NAV_MAP}->{PARM_HASH}->{$self->symb . |
|
'.0.parameter_randompick'}; |
|
} |
|
sub src { |
|
my $self=shift; |
|
return $self->navHash("src_", 1); |
|
} |
|
sub symb { |
|
my $self=shift; |
|
(my $first, my $second) = $self->{ID} =~ /(\d+).(\d+)/; |
|
my $symbSrc = &Apache::lonnet::declutter($self->src()); |
|
return &Apache::lonnet::declutter( |
|
$self->navHash('map_id_'.$first)) |
|
. '___' . $second . '___' . $symbSrc; |
|
} |
|
sub title { my $self=shift; return $self->navHash("title_", 1); } |
|
sub to { my $self=shift; return $self->navHash("to_", 1); } |
|
sub compTitle { |
|
my $self = shift; |
|
my $title = $self->title(); |
|
$title=~s/\&colon\;/\:/gs; |
|
if (!$title) { |
|
$title = $self->src(); |
|
$title = substr($title, rindex($title, '/') + 1); |
|
} |
|
return $title; |
|
} |
|
=pod |
|
|
|
B<Predicate Testing the Resource> |
|
|
|
These methods are shortcuts to deciding if a given resource has a given property. |
|
|
|
=over 4 |
|
|
|
=item * B<is_map>: |
|
|
|
Returns true if the resource is a map type. |
|
|
|
=item * B<is_problem>: |
|
|
|
Returns true if the resource is a problem type, false |
|
otherwise. (Looks at the extension on the src field; might need more |
|
to work correctly.) |
|
|
|
=item * B<is_page>: |
|
|
|
Returns true if the resource is a page. |
|
|
|
=item * B<is_sequence>: |
|
|
|
Returns true if the resource is a sequence. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
|
|
sub is_html { |
|
my $self=shift; |
|
my $src = $self->src(); |
|
return ($src =~ /html$/); |
|
} |
|
sub is_map { my $self=shift; return defined($self->navHash("is_map_", 1)); } |
|
sub is_page { |
|
my $self=shift; |
|
my $src = $self->src(); |
|
return ($src =~ /page$/); |
|
} |
|
sub is_problem { |
|
my $self=shift; |
|
my $src = $self->src(); |
|
return ($src =~ /problem$/); |
|
} |
|
sub is_sequence { |
|
my $self=shift; |
|
my $src = $self->src(); |
|
return ($src =~ /sequence$/); |
|
} |
|
|
|
# Private method: Shells out to the parmval in the nav map, handler parts. |
|
sub parmval { |
|
my $self = shift; |
|
my $what = shift; |
|
my $part = shift || "0"; |
|
return $self->{NAV_MAP}->parmval($part.'.'.$what, $self->symb()); |
|
} |
|
|
|
=pod |
|
|
|
B<Map Methods> |
|
|
|
These methods are useful for getting information about the map |
|
properties of the resource, if the resource is a map (B<is_map>). |
|
|
|
=over 4 |
|
|
|
=item * B<map_finish>: |
|
|
|
Returns a reference to a resource object corresponding to the finish |
|
resource of the map. |
|
|
|
=item * B<map_pc>: |
|
|
|
Returns the pc value of the map, which is the first number that |
|
appears in the resource ID of the resources in the map, and is the |
|
number that appears around the middle of the symbs of the resources in |
|
that map. |
|
|
|
=item * B<map_start>: |
|
|
|
Returns a reference to a resource object corresponding to the start |
|
resource of the map. |
|
|
|
=item * B<map_type>: |
|
|
|
Returns a string with the type of the map in it. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub map_finish { |
|
my $self = shift; |
|
my $src = $self->src(); |
|
$src = Apache::lonnet::clutter($src); |
|
my $res = $self->navHash("map_finish_$src", 0); |
|
$res = $self->{NAV_MAP}->getById($res); |
|
return $res; |
|
} |
|
sub map_pc { |
|
my $self = shift; |
|
my $src = $self->src(); |
|
return $self->navHash("map_pc_$src", 0); |
|
} |
|
sub map_start { |
|
my $self = shift; |
|
my $src = $self->src(); |
|
$src = Apache::lonnet::clutter($src); |
|
my $res = $self->navHash("map_start_$src", 0); |
|
$res = $self->{NAV_MAP}->getById($res); |
|
return $res; |
|
} |
|
sub map_type { |
|
my $self = shift; |
|
my $pc = $self->map_pc(); |
|
return $self->navHash("map_type_$pc", 0); |
|
} |
|
|
|
|
|
|
|
##### |
|
# Property queries |
|
##### |
|
|
|
# These functions will be responsible for returning the CORRECT |
|
# VALUE for the parameter, no matter what. So while they may look |
|
# like direct calls to parmval, they can be more then that. |
|
# So, for instance, the duedate function should use the "duedatetype" |
|
# information, rather then the resource object user. |
|
|
|
=pod |
|
|
|
=head2 Resource Parameters |
|
|
|
In order to use the resource parameters correctly, the nav map must |
|
have been instantiated with genCourseAndUserOptions set to true, so |
|
the courseopt and useropt is read correctly. Then, you can call these |
|
functions to get the relevant parameters for the resource. Each |
|
function defaults to part "0", but can be directed to another part by |
|
passing the part as the parameter. |
|
|
|
These methods are responsible for getting the parameter correct, not |
|
merely reflecting the contents of the GDBM hashes. As we move towards |
|
dates relative to other dates, these methods should be updated to |
|
reflect that. (Then, anybody using these methods will not have to update |
|
their code.) |
|
|
|
=over 4 |
|
|
|
=item * B<acc>: |
|
|
|
Get the Client IP/Name Access Control information. |
|
|
|
=item * B<answerdate>: |
|
|
|
Get the answer-reveal date for the problem. |
|
|
|
=item * B<duedate>: |
|
|
|
Get the due date for the problem. |
|
|
|
=item * B<tries>: |
|
|
|
Get the number of tries the student has used on the problem. |
|
|
|
=item * B<maxtries>: |
|
|
|
Get the number of max tries allowed. |
|
|
|
=item * B<opendate>: |
|
|
|
Get the open date for the problem. |
|
|
|
=item * B<sig>: |
|
|
|
Get the significant figures setting. |
|
|
|
=item * B<tol>: |
|
|
|
Get the tolerance for the problem. |
|
|
|
=item * B<tries>: |
|
|
|
Get the number of tries the user has already used on the problem. |
|
|
|
=item * B<type>: |
|
|
|
Get the question type for the problem. |
|
|
|
=item * B<weight>: |
|
|
|
Get the weight for the problem. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub acc { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("acc", $part); |
|
} |
|
sub answerdate { |
|
(my $self, my $part) = @_; |
|
# Handle intervals |
|
if ($self->parmval("answerdate.type", $part) eq 'date_interval') { |
|
return $self->duedate($part) + |
|
$self->parmval("answerdate", $part); |
|
} |
|
return $self->parmval("answerdate", $part); |
|
} |
|
sub awarded { my $self = shift; return $self->queryRestoreHash('awarded', shift); } |
|
sub duedate { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("duedate", $part); |
|
} |
|
sub maxtries { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("maxtries", $part); |
|
} |
|
sub opendate { |
|
(my $self, my $part) = @_; |
|
if ($self->parmval("opendate.type", $part) eq 'date_interval') { |
|
return $self->duedate($part) - |
|
$self->parmval("opendate", $part); |
|
} |
|
return $self->parmval("opendate"); |
|
} |
|
sub sig { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("sig", $part); |
|
} |
|
sub tol { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("tol", $part); |
|
} |
|
sub tries { |
|
my $self = shift; |
|
my $tries = $self->queryRestoreHash('tries', shift); |
|
if (!defined($tries)) { return '0';} |
|
return $tries; |
|
} |
|
sub type { |
|
(my $self, my $part) = @_; |
|
return $self->parmval("type", $part); |
|
} |
|
sub weight { |
|
my $self = shift; my $part = shift; |
|
return $self->parmval("weight", $part); |
|
} |
|
|
|
# Multiple things need this |
|
sub getReturnHash { |
|
my $self = shift; |
|
|
|
if (!defined($self->{RETURN_HASH})) { |
|
my %tmpHash = &Apache::lonnet::restore($self->symb()); |
|
$self->{RETURN_HASH} = \%tmpHash; |
|
} |
|
} |
|
|
|
###### |
|
# Status queries |
|
###### |
|
|
|
# These methods query the status of problems. |
|
|
|
# If we need to count parts, this function determines the number of |
|
# parts from the metadata. When called, it returns a reference to a list |
|
# of strings corresponding to the parts. (Thus, using it in a scalar context |
|
# tells you how many parts you have in the problem: |
|
# $partcount = scalar($resource->countParts()); |
|
# Don't use $self->{PARTS} directly because you don't know if it's been |
|
# computed yet. |
|
|
|
=pod |
|
|
|
=head2 Resource misc |
|
|
|
Misc. functions for the resource. |
|
|
|
=over 4 |
|
|
|
=item * B<hasDiscussion>: |
|
|
|
Returns a false value if there has been discussion since the user last |
|
logged in, true if there has. Always returns false if the discussion |
|
data was not extracted when the nav map was constructed. |
|
|
|
=item * B<getFeedback>: |
|
|
|
Gets the feedback for the resource and returns the raw feedback string |
|
for the resource, or the null string if there is no feedback or the |
|
email data was not extracted when the nav map was constructed. Usually |
|
used like this: |
|
|
|
for (split(/\,/, $res->getFeedback())) { |
|
my $link = &Apache::lonnet::escape($_); |
|
... |
|
|
|
and use the link as appropriate. |
|
|
|
=cut |
|
|
|
sub hasDiscussion { |
|
my $self = shift; |
|
return $self->{NAV_MAP}->hasDiscussion($self->symb()); |
|
} |
|
|
|
sub getFeedback { |
|
my $self = shift; |
|
my $source = $self->src(); |
|
if ($source =~ /^\/res\//) { $source = substr $source, 5; } |
|
return $self->{NAV_MAP}->getFeedback($source); |
|
} |
|
|
|
sub getErrors { |
|
my $self = shift; |
|
my $source = $self->src(); |
|
if ($source =~ /^\/res\//) { $source = substr $source, 5; } |
|
return $self->{NAV_MAP}->getErrors($source); |
|
} |
|
|
|
=pod |
|
|
|
=item * B<parts>(): |
|
|
|
Returns a list reference containing sorted strings corresponding to |
|
each part of the problem. To count the number of parts, use the list |
|
in a scalar context, and subtract one if greater than two. (One part |
|
problems have a part 0. Multi-parts have a part 0, plus a part for |
|
each part. Filtering part 0 if you want it is up to you.) |
|
|
|
=item * B<countParts>(): |
|
|
|
Returns the number of parts of the problem a student can answer. Thus, |
|
for single part problems, returns 1. For multipart, it returns the |
|
number of parts in the problem, not including psuedo-part 0. Thus, |
|
B<parts> may return an array with fewer parts in it then countParts |
|
might lead you to believe. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub parts { |
|
my $self = shift; |
|
|
|
if ($self->ext) { return ['0']; } |
|
|
|
$self->extractParts(); |
|
return $self->{PARTS}; |
|
} |
|
|
|
sub countParts { |
|
my $self = shift; |
|
|
|
my $parts = $self->parts(); |
|
my $delta = 0; |
|
for my $part (@$parts) { |
|
if ($part eq '0') { $delta--; } |
|
} |
|
|
|
if ($self->{RESOURCE_ERROR}) { |
|
return 0; |
|
} |
|
|
|
return scalar(@{$parts}) + $delta; |
|
} |
|
|
|
# Private function: Extracts the parts information and saves it |
|
sub extractParts { |
|
my $self = shift; |
|
|
|
return if (defined($self->{PARTS})); |
|
return if ($self->ext); |
|
|
|
$self->{PARTS} = []; |
|
|
|
# Retrieve part count, if this is a problem |
|
if ($self->is_problem()) { |
|
my $metadata = &Apache::lonnet::metadata($self->src(), 'packages'); |
|
if (!$metadata) { |
|
$self->{RESOURCE_ERROR} = 1; |
|
$self->{PARTS} = []; |
|
return; |
|
} |
|
foreach (split(/\,/,$metadata)) { |
|
if ($_ =~ /^part_(.*)$/) { |
|
my $part = $1; |
|
# check to see if part is turned off. |
|
if (! Apache::loncommon::check_if_partid_hidden($part, $self->symb())) { |
|
push @{$self->{PARTS}}, $1; |
|
} |
|
} |
|
} |
|
|
|
|
|
my @sortedParts = sort @{$self->{PARTS}}; |
|
$self->{PARTS} = \@sortedParts; |
|
} |
|
|
|
return; |
|
} |
|
|
|
=pod |
|
|
|
=head2 Resource Status |
|
|
|
Problem resources have status information, reflecting their various |
|
dates and completion statuses. |
|
|
|
There are two aspects to the status: the date-related information and |
|
the completion information. |
|
|
|
Idiomatic usage of these two methods would probably look something |
|
like |
|
|
|
foreach ($resource->parts()) { |
|
my $dateStatus = $resource->getDateStatus($_); |
|
my $completionStatus = $resource->getCompletionStatus($_); |
|
|
|
or |
|
|
|
my $status = $resource->status($_); |
|
|
|
... use it here ... |
|
} |
|
|
|
Which you use depends on exactly what you are looking for. The |
|
status() function has been optimized for the nav maps display and may |
|
not precisely match what you need elsewhere. |
|
|
|
The symbolic constants shown below can be accessed through the |
|
resource object: C<$res->OPEN>. |
|
|
|
=over 4 |
|
|
|
=item * B<getDateStatus>($part): |
|
|
|
($part defaults to 0). A convenience function that returns a symbolic |
|
constant telling you about the date status of the part. The possible |
|
return values are: |
|
|
|
=back |
|
|
|
B<Date Codes> |
|
|
|
=over 4 |
|
|
|
=item * B<OPEN_LATER>: |
|
|
|
The problem will be opened later. |
|
|
|
=item * B<OPEN>: |
|
|
|
Open and not yet due. |
|
|
|
|
|
=item * B<PAST_DUE_ANSWER_LATER>: |
|
|
|
The due date has passed, but the answer date has not yet arrived. |
|
|
|
=item * B<PAST_DUE_NO_ANSWER>: |
|
|
|
The due date has passed and there is no answer opening date set. |
|
|
|
=item * B<ANSWER_OPEN>: |
|
|
|
The answer date is here. |
|
|
|
=item * B<NETWORK_FAILURE>: |
|
|
|
The information is unknown due to network failure. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
# Apparently the compiler optimizes these into constants automatically |
|
sub OPEN_LATER { return 0; } |
|
sub OPEN { return 1; } |
|
sub PAST_DUE_NO_ANSWER { return 2; } |
|
sub PAST_DUE_ANSWER_LATER { return 3; } |
|
sub ANSWER_OPEN { return 4; } |
|
sub NOTHING_SET { return 5; } |
|
sub NETWORK_FAILURE { return 100; } |
|
|
|
# getDateStatus gets the date status for a given problem part. |
|
# Because answer date, due date, and open date are fully independent |
|
# (i.e., it is perfectly possible to *only* have an answer date), |
|
# we have to completely cover the 3x3 maxtrix of (answer, due, open) x |
|
# (past, future, none given). This function handles this with a decision |
|
# tree. Read the comments to follow the decision tree. |
|
|
|
sub getDateStatus { |
|
my $self = shift; |
|
my $part = shift; |
|
$part = "0" if (!defined($part)); |
|
|
|
# Always return network failure if there was one. |
|
return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE}); |
|
|
|
my $now = time(); |
|
|
|
my $open = $self->opendate($part); |
|
my $due = $self->duedate($part); |
|
my $answer = $self->answerdate($part); |
|
|
|
if (!$open && !$due && !$answer) { |
|
# no data on the problem at all |
|
# should this be the same as "open later"? think multipart. |
|
return $self->NOTHING_SET; |
|
} |
|
if (!$open || $now < $open) {return $self->OPEN_LATER} |
|
if (!$due || $now < $due) {return $self->OPEN} |
|
if ($answer && $now < $answer) {return $self->PAST_DUE_ANSWER_LATER} |
|
if ($answer) { return $self->ANSWER_OPEN; } |
|
return PAST_DUE_NO_ANSWER; |
|
} |
|
|
|
=pod |
|
|
|
B<> |
|
|
|
=over 4 |
|
|
|
=item * B<getCompletionStatus>($part): |
|
|
|
($part defaults to 0.) A convenience function that returns a symbolic |
|
constant telling you about the completion status of the part, with the |
|
following possible results: |
|
|
|
=back |
|
|
|
B<Completion Codes> |
|
|
|
=over 4 |
|
|
|
=item * B<NOT_ATTEMPTED>: |
|
|
|
Has not been attempted at all. |
|
|
|
=item * B<INCORRECT>: |
|
|
|
Attempted, but wrong by student. |
|
|
|
=item * B<INCORRECT_BY_OVERRIDE>: |
|
|
|
Attempted, but wrong by instructor override. |
|
|
|
=item * B<CORRECT>: |
|
|
|
Correct or correct by instructor. |
|
|
|
=item * B<CORRECT_BY_OVERRIDE>: |
|
|
|
Correct by instructor override. |
|
|
|
=item * B<EXCUSED>: |
|
|
|
Excused. Not yet implemented. |
|
|
|
=item * B<NETWORK_FAILURE>: |
|
|
|
Information not available due to network failure. |
|
|
|
=item * B<ATTEMPTED>: |
|
|
|
Attempted, and not yet graded. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub NOT_ATTEMPTED { return 10; } |
|
sub INCORRECT { return 11; } |
|
sub INCORRECT_BY_OVERRIDE { return 12; } |
|
sub CORRECT { return 13; } |
|
sub CORRECT_BY_OVERRIDE { return 14; } |
|
sub EXCUSED { return 15; } |
|
sub ATTEMPTED { return 16; } |
|
|
|
sub getCompletionStatus { |
|
my $self = shift; |
|
return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE}); |
|
|
|
my $status = $self->queryRestoreHash('solved', shift); |
|
|
|
# Left as seperate if statements in case we ever do more with this |
|
if ($status eq 'correct_by_student') {return $self->CORRECT;} |
|
if ($status eq 'correct_by_override') {return $self->CORRECT_BY_OVERRIDE; } |
|
if ($status eq 'incorrect_attempted') {return $self->INCORRECT; } |
|
if ($status eq 'incorrect_by_override') {return $self->INCORRECT_BY_OVERRIDE; } |
|
if ($status eq 'excused') {return $self->EXCUSED; } |
|
if ($status eq 'ungraded_attempted') {return $self->ATTEMPTED; } |
|
return $self->NOT_ATTEMPTED; |
|
} |
|
|
|
sub queryRestoreHash { |
|
my $self = shift; |
|
my $hashentry = shift; |
|
my $part = shift; |
|
$part = "0" if (!defined($part)); |
|
return $self->NETWORK_FAILURE if ($self->{NAV_MAP}->{NETWORK_FAILURE}); |
|
|
|
$self->getReturnHash(); |
|
|
|
return $self->{RETURN_HASH}->{'resource.'.$part.'.'.$hashentry}; |
|
} |
|
|
|
=pod |
|
|
|
B<Composite Status> |
|
|
|
Along with directly returning the date or completion status, the |
|
resource object includes a convenience function B<status>() that will |
|
combine the two status tidbits into one composite status that can |
|
represent the status of the resource as a whole. The precise logic is |
|
documented in the comments of the status method. The following results |
|
may be returned, all available as methods on the resource object |
|
($res->NETWORK_FAILURE): |
|
|
|
=over 4 |
|
|
|
=item * B<NETWORK_FAILURE>: |
|
|
|
The network has failed and the information is not available. |
|
|
|
=item * B<NOTHING_SET>: |
|
|
|
No dates have been set for this problem (part) at all. (Because only |
|
certain parts of a multi-part problem may be assigned, this can not be |
|
collapsed into "open later", as we do not know a given part will EVER |
|
be opened. For single part, this is the same as "OPEN_LATER".) |
|
|
|
=item * B<CORRECT>: |
|
|
|
For any reason at all, the part is considered correct. |
|
|
|
=item * B<EXCUSED>: |
|
|
|
For any reason at all, the problem is excused. |
|
|
|
=item * B<PAST_DUE_NO_ANSWER>: |
|
|
|
The problem is past due, not considered correct, and no answer date is |
|
set. |
|
|
|
=item * B<PAST_DUE_ANSWER_LATER>: |
|
|
|
The problem is past due, not considered correct, and an answer date in |
|
the future is set. |
|
|
|
=item * B<ANSWER_OPEN>: |
|
|
|
The problem is past due, not correct, and the answer is now available. |
|
|
|
=item * B<OPEN_LATER>: |
|
|
|
The problem is not yet open. |
|
|
|
=item * B<TRIES_LEFT>: |
|
|
|
The problem is open, has been tried, is not correct, but there are |
|
tries left. |
|
|
|
=item * B<INCORRECT>: |
|
|
|
The problem is open, and all tries have been used without getting the |
|
correct answer. |
|
|
|
=item * B<OPEN>: |
|
|
|
The item is open and not yet tried. |
|
|
|
=item * B<ATTEMPTED>: |
|
|
|
The problem has been attempted. |
|
|
|
=back |
|
|
|
=cut |
|
|
|
sub TRIES_LEFT { return 10; } |
|
|
|
sub status { |
|
my $self = shift; |
|
my $part = shift; |
|
if (!defined($part)) { $part = "0"; } |
|
my $completionStatus = $self->getCompletionStatus($part); |
|
my $dateStatus = $self->getDateStatus($part); |
|
|
|
# What we have is a two-dimensional matrix with 4 entries on one |
|
# dimension and 5 entries on the other, which we want to colorize, |
|
# plus network failure and "no date data at all". |
|
|
|
if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } |
|
|
|
# There are a few whole rows we can dispose of: |
|
if ($completionStatus == CORRECT || |
|
$completionStatus == CORRECT_BY_OVERRIDE ) { |
|
return CORRECT; |
|
} |
|
|
|
if ($completionStatus == ATTEMPTED) { |
|
return ATTEMPTED; |
|
} |
|
|
|
# If it's EXCUSED, then return that no matter what |
|
if ($completionStatus == EXCUSED) { |
|
return EXCUSED; |
|
} |
|
|
|
if ($dateStatus == NOTHING_SET) { |
|
return NOTHING_SET; |
|
} |
|
|
|
# Now we're down to a 4 (incorrect, incorrect_override, not_attempted) |
|
# by 4 matrix (date statuses). |
|
|
|
if ($dateStatus == PAST_DUE_ANSWER_LATER || |
|
$dateStatus == PAST_DUE_NO_ANSWER ) { |
|
return $dateStatus; |
|
} |
|
|
|
if ($dateStatus == ANSWER_OPEN) { |
|
return ANSWER_OPEN; |
|
} |
|
|
|
# Now: (incorrect, incorrect_override, not_attempted) x |
|
# (open_later), (open) |
|
|
|
if ($dateStatus == OPEN_LATER) { |
|
return OPEN_LATER; |
|
} |
|
|
|
# If it's WRONG... |
|
if ($completionStatus == INCORRECT || $completionStatus == INCORRECT_BY_OVERRIDE) { |
|
# and there are TRIES LEFT: |
|
if ($self->tries($part) < $self->maxtries($part) || !$self->maxtries($part)) { |
|
return TRIES_LEFT; |
|
} |
|
return INCORRECT; # otherwise, return orange; student can't fix this |
|
} |
|
|
|
# Otherwise, it's untried and open |
|
return OPEN; |
|
} |
|
|
|
=pod |
|
|
|
=head2 Resource/Nav Map Navigation |
|
|
|
=over 4 |
|
|
|
=item * B<getNext>(): |
|
|
|
Retreive an array of the possible next resources after this |
|
one. Always returns an array, even in the one- or zero-element case. |
|
|
|
=item * B<getPrevious>(): |
|
|
|
Retreive an array of the possible previous resources from this |
|
one. Always returns an array, even in the one- or zero-element case. |
|
|
|
=cut |
|
|
|
sub getNext { |
|
my $self = shift; |
|
my @branches; |
|
my $to = $self->to(); |
|
foreach my $branch ( split(/,/, $to) ) { |
|
my $choice = $self->{NAV_MAP}->getById($branch); |
|
my $next = $choice->goesto(); |
|
$next = $self->{NAV_MAP}->getById($next); |
|
|
|
push @branches, $next; |
|
} |
|
return \@branches; |
|
} |
|
|
|
sub getPrevious { |
|
my $self = shift; |
|
my @branches; |
|
my $from = $self->from(); |
|
foreach my $branch ( split /,/, $from) { |
|
my $choice = $self->{NAV_MAP}->getById($branch); |
|
my $prev = $choice->comesfrom(); |
|
$prev = $self->{NAV_MAP}->getById($prev); |
|
|
|
push @branches, $prev; |
|
} |
|
return \@branches; |
|
} |
|
|
|
sub browsePriv { |
|
my $self = shift; |
|
if (defined($self->{BROWSE_PRIV})) { |
|
return $self->{BROWSE_PRIV}; |
|
} |
|
|
|
$self->{BROWSE_PRIV} = &Apache::lonnet::allowed('bre', $self->src()); |
|
} |
|
|
|
=pod |
|
|
|
=back |
|
|
|
=cut |
|
|
|
1; |
|
|
|
__END__ |
|
|
|
|