Key: ';
if ($navmap->{LAST_CHECK}) {
$result .=
- ' New discussion since '.
+ ' '.&mt('New discussion since').' '.
strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})).
' '.
- ' New message (click to open)'.
+ ' '.&mt('New message (click to open)').'
'.
'
';
} else {
$result .= ' '.
- ' Discussions '.
- ' New message (click to open)'.
+ ' '.&mt('Discussions').' '.
+ ' '.&mt('New message (click to open)').
' ';
}
@@ -1242,15 +1525,52 @@ sub render {
if ($condition) {
$result.="Close All Folders ";
+ "\">".&mt('Close All Folders')."";
} else {
$result.="Open All Folders ";
+ "\">".&mt('Open All Folders')."";
}
- $result .= " \n";
- }
+ $result .= "\n";
+ }
+
+ # Check for any unread discussions in all resources.
+ if (!$args->{'resource_no_folder_link'}) {
+ my $totdisc = 0;
+ my $haveDisc = '';
+ my @allres=$navmap->retrieveResources();
+ foreach my $resource (@allres) {
+ if ($resource->hasDiscussion()) {
+ my $ressymb;
+ if ($resource->symb() =~ m-(___adm/\w+/\w+)/(\d+)/bulletinboard$-) {
+ $ressymb = 'bulletin___'.$2.$1.'/'.$2.'/bulletinboard';
+ } else {
+ $ressymb = $resource->symb();
+ }
+ $haveDisc .= $ressymb.':';
+ $totdisc ++;
+ }
+ }
+ if ($totdisc > 0) {
+ $haveDisc =~ s/:$//;
+ my %lt = &Apache::lonlocal::texthash(
+ 'mapr' => 'Mark all posts read',
+ );
+ $result .= (<$lt{'mapr'}
+
+END
+ } else {
+ $result .= ' ';
+ }
+ }
+
+ if ($args->{'sort_html'}) { $result.=$args->{'sort_html'}; }
+ $result .= " \n";
if ($r) {
$r->print($result);
$r->rflush();
@@ -1282,7 +1602,7 @@ sub render {
$it->{FIRST_RESOURCE},
$it->{FINISH_RESOURCE},
{}, undef, 1);
- $depth = 0;
+ my $depth = 0;
$dfsit->next();
my $curRes = $dfsit->next();
while ($depth > -1) {
@@ -1291,7 +1611,7 @@ sub render {
if (ref($curRes)) {
# Parallel pre-processing: Do sequences have non-filtered-out children?
- if ($curRes->is_sequence()) {
+ if ($curRes->is_map()) {
$curRes->{DATA}->{HAS_VISIBLE_CHILDREN} = 0;
# Sequences themselves do not count as visible children,
# unless those sequences also have visible children.
@@ -1314,9 +1634,6 @@ sub render {
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;
@@ -1324,9 +1641,43 @@ sub render {
# export "here" marker information
$args->{'here'} = $here;
- while ($depth > 0) {
- if ($curRes == $it->BEGIN_MAP()) { $depth++; }
- if ($curRes == $it->END_MAP()) { $depth--; }
+ $args->{'indentLevel'} = -1; # first BEGIN_MAP takes this to 0
+ my @resources;
+ my $code='';# sub { !(shift->is_map();) };
+ if ($args->{'sort'} eq 'title') {
+ my $oldFilterFunc = $filterFunc;
+ my $filterFunc=
+ sub {
+ my ($res)=@_;
+ if ($res->is_map()) { return 0;}
+ return &$oldFilterFunc($res);
+ };
+ @resources=$navmap->retrieveResources(undef,$filterFunc);
+ @resources= sort {lc($a->compTitle) cmp lc($b->compTitle)} @resources;
+ } elsif ($args->{'sort'} eq 'duedate') {
+ @resources=$navmap->retrieveResources(undef,
+ sub { shift->is_problem(); });
+ @resources= sort
+ {
+ if ($a->duedate ne $b->duedate) {
+ return $a->duedate cmp $b->duedate;
+ } else {
+ lc($a->compTitle) cmp lc($b->compTitle)
+ }
+ } @resources;
+ } else {
+ #unknow sort mechanism or default
+ undef($args->{'sort'});
+ }
+
+
+ while (1) {
+ if ($args->{'sort'}) {
+ $curRes = shift(@resources);
+ } else {
+ $curRes = $it->next($closeAllPages);
+ }
+ if (!$curRes) { last; }
# Maintain indentation level.
if ($curRes == $it->BEGIN_MAP() ||
@@ -1354,7 +1705,7 @@ sub render {
}
# If this is an empty sequence and we're filtering them, continue on
- if ($curRes->is_sequence() && $args->{'suppressEmptySequences'} &&
+ if ($curRes->is_map() && $args->{'suppressEmptySequences'} &&
!$curRes->{DATA}->{HAS_VISIBLE_CHILDREN}) {
next;
}
@@ -1413,8 +1764,13 @@ sub render {
@parts = ($parts[0]);
$args->{'condensed'} = 1;
}
-
}
+ # Multipart problem with one part: always "condense" (happens
+ # to match the desirable behavior)
+ if ($curRes->countParts() == 1) {
+ @parts = ($parts[0]);
+ $args->{'condensed'} = 1;
+ }
}
}
@@ -1436,14 +1792,23 @@ sub render {
# 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 $src;
+ if ($args->{'sort'}) {
+ $src = $curRes->src(); # FIXME this is wrong for .pages
+ } else {
+ my $stack = $it->getStack();
+ $src=getLinkForResource($stack);
+ }
+ my $anchor='';
+ if ($src=~s/(\#.*)$//) {
+ $anchor=$1;
+ }
my $srcHasQuestion = $src =~ /\?/;
$args->{"resourceLink"} = $src.
($srcHasQuestion?'&':'?') .
- 'symb=' . &Apache::lonnet::escape($curRes->symb());
-
+ 'symb=' . &Apache::lonnet::escape($curRes->symb()).
+ $anchor;
+
# Now, display each column.
foreach my $col (@$cols) {
my $colHTML = '';
@@ -1475,7 +1840,14 @@ sub render {
$r->rflush();
}
} continue {
- $curRes = $it->next();
+ if ($r) {
+ # If we have the connection, make sure the user is still connected
+ my $c = $r->connection;
+ if ($c->aborted()) {
+ # Who cares what we do, nobody will see it anyhow.
+ return '';
+ }
+ }
}
# Print out the part that jumps to #curloc if it exists
@@ -1486,7 +1858,12 @@ sub render {
# it's quite likely this might fix other browsers, too, and
# certainly won't hurt anything.
if ($displayedJumpMarker) {
- $result .= "\n";
+ $result .= "
+";
}
$result .= "";
@@ -1508,41 +1885,64 @@ 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: Apache::lonnavmaps::navmap
-=head1 Object: navmap
+=head2 Overview
-Encapsulating the compiled nav map
+The navmap object's job is to provide access to the resources
+in the course as Apache::lonnavmaps::resource objects, and to
+query and manage the relationship between those resource objects.
-navmap is an object that encapsulates a compiled course map and
-provides a reasonable interface to it.
+Generally, you'll use the navmap object in one of three basic ways.
+In order of increasing complexity and power:
-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.
+=over 4
-You must obtain resource objects through the navmap object.
+=item * C<$navmap-EgetByX>, where X is B, B, B or B. This provides
+ various ways to obtain resource objects, based on various identifiers.
+ Use this when you want to request information about one object or
+ a handful of resources you already know the identities of, from some
+ other source. For more about Ids, Symbs, and MapPcs, see the
+ Resource documentation. Note that Url should be a B,
+ not your first choice; it only works when there is only one
+ instance of the resource in the course, which only applies to
+ maps, and even that may change in the future.
+
+=item * CretrieveResources(args)>. This
+ retrieves resources matching some criterion and returns them
+ in a flat array, with no structure information. Use this when
+ you are manipulating a series of resources, based on what map
+ the are in, but do not care about branching, or exactly how
+ the maps and resources are related. This is the most common case.
+
+=item * C<$it = $navmap-EgetIterator(args)>. This allows you traverse
+ the course's navmap in various ways without writing the traversal
+ code yourself. See iterator documentation below. Use this when
+ you need to know absolutely everything about the course, including
+ branches and the precise relationship between maps and resources.
-=head2 Methods
+=back
+
+=head2 Creation And Destruction
+
+To create a navmap object, use the following function:
=over 4
-=item * B(navHashFile, parmHashFile, genCourseAndUserOptions,
- genMailDiscussStatus):
+=item * Bnew>():
+
+Creates a new navmap object. Returns the navmap object if this is
+successful, or B if not.
+
+=back
-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 if not. You must check for undef; errors will occur when you
-try to use the other methods otherwise.
+When you are done with the $navmap object, you I call
+$navmap->untieHashes(), or you'll prevent the current user from using that
+course until the web server is restarted. (!)
+
+=head2 Methods
+
+=over 4
=item * B(first, finish, filter, condition):
@@ -1559,11 +1959,6 @@ sub new {
my $class = ref($proto) || $proto;
my $self = {};
- $self->{NAV_HASH_FILE} = shift;
- $self->{PARM_HASH_FILE} = shift;
- $self->{GENERATE_COURSE_USER_OPT} = shift;
- $self->{GENERATE_EMAIL_DISCUSS_STATUS} = shift;
-
# Resource cache stores navmap resources as we reference them. We generate
# them on-demand so we don't pay for creating resources unless we use them.
$self->{RESOURCE_CACHE} = {};
@@ -1576,12 +1971,13 @@ sub new {
my %navmaphash;
my %parmhash;
- if (!(tie(%navmaphash, 'GDBM_File', $self->{NAV_HASH_FILE},
+ my $courseFn = $ENV{"request.course.fn"};
+ if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db",
&GDBM_READER(), 0640))) {
return undef;
}
- if (!(tie(%parmhash, 'GDBM_File', $self->{PARM_HASH_FILE},
+ if (!(tie(%parmhash, 'GDBM_File', "${courseFn}_parms.db",
&GDBM_READER(), 0640)))
{
untie %{$self->{PARM_HASH}};
@@ -1590,120 +1986,145 @@ sub new {
$self->{NAV_HASH} = \%navmaphash;
$self->{PARM_HASH} = \%parmhash;
- $self->{INITED} = 0;
+ $self->{PARM_CACHE} = {};
bless($self);
return $self;
}
-sub init {
+sub generate_course_user_opt {
my $self = shift;
- if ($self->{INITED}) { return; }
+ if ($self->{COURSE_USER_OPT_GENERATED}) { return; }
- # If the course opt hash and the user opt hash should be generated,
- # generate them
- if ($self->{GENERATE_COURSE_USER_OPT}) {
- my $uname=$ENV{'user.name'};
- 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);
-
- my $userprefix=$uname.'_'.$udom.'_';
-
- my %courserdatas; my %useropt; my %courseopt; my %userrdatas;
- unless ($uhome eq 'no_host') {
+ my $uname=$ENV{'user.name'};
+ 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);
+
+ my $userprefix=$uname.'_'.$udom.'_';
+
+ my %courserdatas; my %useropt; my %courseopt; my %userrdatas;
+ unless ($uhome eq 'no_host') {
# ------------------------------------------------- Get coursedata (if present)
- unless ((time-$courserdatas{$cid.'.last_cache'})<240) {
- my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum.
- ':resourcedata',$chome);
- # Check for network failure
- if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) {
- $self->{NETWORK_FAILURE} = 1;
- } elsif ($reply!~/^error\:/) {
- $courserdatas{$cid}=$reply;
- $courserdatas{$cid.'.last_cache'}=time;
- }
- }
- foreach (split(/\&/,$courserdatas{$cid})) {
- my ($name,$value)=split(/\=/,$_);
- $courseopt{$userprefix.&Apache::lonnet::unescape($name)}=
- &Apache::lonnet::unescape($value);
- }
+ unless ((time-$courserdatas{$cid.'.last_cache'})<240) {
+ my $reply=&Apache::lonnet::reply('dump:'.$cdom.':'.$cnum.
+ ':resourcedata',$chome);
+ # Check for network failure
+ if ( $reply =~ /no.such.host/i || $reply =~ /con_lost/i) {
+ $self->{NETWORK_FAILURE} = 1;
+ } elsif ($reply!~/^error\:/) {
+ $courserdatas{$cid}=$reply;
+ $courserdatas{$cid.'.last_cache'}=time;
+ }
+ }
+ foreach (split(/\&/,$courserdatas{$cid})) {
+ my ($name,$value)=split(/\=/,$_);
+ $courseopt{$userprefix.&Apache::lonnet::unescape($name)}=
+ &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;
- }
- }
+ 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->{COURSE_USER_OPT_GENERATED} = 1;
+
+ return;
+}
- $self->{PARM_CACHE} = {};
- $self->{INITED} = 1;
+sub generate_email_discuss_status {
+ my $self = shift;
+ my $symb = shift;
+ if ($self->{EMAIL_DISCUSS_GENERATED}) { return; }
+
+ 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 %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss',
+ $ENV{'user.domain'},$ENV{'user.name'},'lastread');
+ my %lastreadtime = ();
+ foreach (keys %lastread) {
+ my $key = $_;
+ $key =~ s/_lastread$//;
+ $lastreadtime{$key} = $lastread{$_};
+ }
+
+ 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->{LAST_READ} = \%lastreadtime;
+
+ $self->{EMAIL_DISCUSS_GENERATED} = 1;
+}
+
+sub get_user_data {
+ my $self = shift;
+ if ($self->{RETRIEVED_USER_DATA}) { return; }
+
+ # Retrieve performance data on problems
+ my %student_data = Apache::lonnet::currentdump($ENV{'request.course.id'},
+ $ENV{'user.domain'},
+ $ENV{'user.name'});
+ $self->{STUDENT_DATA} = \%student_data;
+
+ $self->{RETRIEVED_USER_DATA} = 1;
}
# Internal function: Takes a key to look up in the nav hash and implements internal
@@ -1713,6 +2134,15 @@ sub navhash {
return $self->{NAV_HASH}->{$key};
}
+=pod
+
+=item * B(): Returns true if the course map is defined,
+ false otherwise. Undefined course maps indicate an error somewhere in
+ LON-CAPA, and you will not be able to proceed with using the navmap.
+ See the B screen for an example of using this.
+
+=cut
+
# Checks to see if coursemap is defined, matching test in old lonnavmaps
sub courseMapDefined {
my $self = shift;
@@ -1742,11 +2172,27 @@ sub untieHashes {
sub hasDiscussion {
my $self = shift;
my $symb = shift;
+
+ $self->generate_email_discuss_status();
+
if (!defined($self->{DISCUSSION_TIME})) { return 0; }
#return defined($self->{DISCUSSION_TIME}->{$symb});
- return $self->{DISCUSSION_TIME}->{$symb} >
- $self->{LAST_CHECK};
+
+# backward compatibility (bulletin boards used to be 'wrapped')
+ my $ressymb = $symb;
+ if ($ressymb =~ m|adm/(\w+)/(\w+)/(\d+)/bulletinboard$|) {
+ unless ($ressymb =~ m|adm/wrapper/adm|) {
+ $ressymb = 'bulletin___'.$3.'___adm/wrapper/adm/'.$1.'/'.$2.'/'.$3.'/bulletinboard';
+ }
+ }
+
+ if ( defined ( $self->{LAST_READ}->{$ressymb} ) ) {
+ return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_READ}->{$ressymb};
+ } else {
+# return $self->{DISCUSSION_TIME}->{$ressymb} > $self->{LAST_CHECK}; # v.1.1 behavior
+ return $self->{DISCUSSION_TIME}->{$ressymb} > 0; # in 1.2 will display speech bubble icons for all items with posts until marked as read (even if read in v 1.1).
+ }
}
# Private method: Does the given resource (as a symb string) have
@@ -1756,6 +2202,8 @@ sub getFeedback {
my $self = shift;
my $symb = shift;
+ $self->generate_email_discuss_status();
+
if (!defined($self->{FEEDBACK})) { return ""; }
return $self->{FEEDBACK}->{$symb};
@@ -1765,7 +2213,9 @@ sub getFeedback {
sub getErrors {
my $self = shift;
my $src = shift;
-
+
+ $self->generate_email_discuss_status();
+
if (!defined($self->{ERROR_MSG})) { return ""; }
return $self->{ERROR_MSG}->{$src};
}
@@ -1779,12 +2229,21 @@ object for that resource. This method, o
(as in the resource object) is the only proper way to obtain a
resource object.
+=item * B(symb):
+
+Based on the symb of the resource, get a resource object for that
+resource. This is one of the proper ways to get a resource object.
+
+=item * B(map_pc):
+
+Based on the map_pc of the resource, get a resource object for
+the given map. This is one of the proper ways to get 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;
@@ -1804,9 +2263,22 @@ sub getById {
sub getBySymb {
my $self = shift;
my $symb = shift;
- my ($mapUrl, $id, $filename) = split (/___/, $symb);
+
+ my ($mapUrl, $id, $filename) = &Apache::lonnet::decode_symb($symb);
my $map = $self->getResourceByUrl($mapUrl);
- return $self->getById($map->map_pc() . '.' . $id);
+ my $returnvalue = undef;
+ if (ref($map)) {
+ $returnvalue = $self->getById($map->map_pc() .'.'.$id);
+ }
+ return $returnvalue;
+}
+
+sub getByMapPc {
+ my $self = shift;
+ my $map_pc = shift;
+ my $map_id = $self->{NAV_HASH}->{'map_id_' . $map_pc};
+ $map_id = $self->{NAV_HASH}->{'ids_' . $map_id};
+ return $self->getById($map_id);
}
=pod
@@ -1859,7 +2331,10 @@ sub parmval {
sub parmval_real {
my $self = shift;
- my ($what,$symb) = @_;
+ my ($what,$symb,$recurse) = @_;
+
+ # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated
+ $self->generate_course_user_opt();
my $cid=$ENV{'request.course.id'};
my $csec=$ENV{'request.course.sec'};
@@ -1869,7 +2344,7 @@ sub parmval_real {
unless ($symb) { return ''; }
my $result='';
- my ($mapname,$id,$fn)=split(/\_\_\_/,$symb);
+ my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb);
# ----------------------------------------------------- Cascading lookup scheme
my $rwhat=$what;
@@ -1919,7 +2394,11 @@ sub parmval_real {
# ----------------------------------------------------- fourth , check default
- my $default=&Apache::lonnet::metadata($fn,$rwhat.'.default');
+ my $meta_rwhat=$rwhat;
+ $meta_rwhat=~s/\./_/g;
+ my $default=&Apache::lonnet::metadata($fn,$meta_rwhat);
+ if (defined($default)) { return $default}
+ $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat);
if (defined($default)) { return $default}
# --------------------------------------------------- fifth , cascade up parts
@@ -1931,9 +2410,12 @@ sub parmval_real {
my $id=pop(@parts);
my $part=join('_',@parts);
if ($part eq '') { $part='0'; }
- my $partgeneral=$self->parmval($part.".$qualifier",$symb);
+ my $partgeneral=$self->parmval($part.".$qualifier",$symb,1);
if (defined($partgeneral)) { return $partgeneral; }
}
+ if ($recurse) { return undef; }
+ my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$what);
+ if (defined($pack_def)) { return $pack_def; }
return '';
}
@@ -1971,7 +2453,7 @@ want to know is if I resources matc
parameter will allow you to avoid potentially expensive enumeration of
all matching resources.
-=item * B(map, filterFunc, recursive):
+=item * B(map, filterFunc, recursive):
Convience method for
@@ -1982,6 +2464,7 @@ in the filter function.
=cut
+
sub getResourceByUrl {
my $self = shift;
my $resUrl = shift;
@@ -2014,31 +2497,27 @@ sub retrieveResources {
$map = $self->getResourceByUrl($map);
}
+ # If nothing was passed, assume top-level map
+ if (!$map) {
+ $map = $self->getById('0.0');
+ }
+
# Check the map's validity.
- if (!$map || !$map->is_map()) {
+ if (!$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);
+ undef, $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--;
- }
-
+ my $curRes;
+
+ while ($curRes = $it->next()) {
if (ref($curRes)) {
if (!&$filterFunc($curRes)) {
next;
@@ -2051,7 +2530,6 @@ sub retrieveResources {
}
}
- $curRes = $it->next();
}
return @resources;
@@ -2103,7 +2581,7 @@ corresponds to where you want the iterat
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
+hash: If a 0, then only resources 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
@@ -2127,21 +2605,28 @@ new branch. The possible tokens are:
=over 4
-=item * BEGIN_MAP:
+=item * B:
+
+The iterator has returned all that it's going to. Further calls to the
+iterator will just produce more of these. This is a "false" value, and
+is the only false value the iterator which will be returned, so it can
+be used as a loop sentinel.
+
+=item * B:
A new map is being recursed into. This is returned I the map
resource itself is returned.
-=item * END_MAP:
+=item * B:
The map is now done.
-=item * BEGIN_BRANCH:
+=item * B:
A branch is now starting. The next resource returned will be the first
in that branch.
-=item * END_BRANCH:
+=item * B:
The branch is now done.
@@ -2159,10 +2644,31 @@ but only one resource will be returned.
=back
+=head2 Normal Usage
+
+Normal usage of the iterator object is to do the following:
+
+ my $it = $navmap->getIterator([your params here]);
+ my $curRes;
+ while ($curRes = $it->next()) {
+ [your logic here]
+ }
+
+Note that inside of the loop, it's frequently useful to check if
+"$curRes" is a reference or not with the reference function; only
+resource objects will be references, and any non-references will
+be the tokens described above.
+
+Also note there is some old code floating around that trys to track
+the depth of the iterator to see when it's done; do not copy that
+code. It is difficult to get right and harder to understand then
+this. They should be migrated to this new style.
+
=cut
# Here are the tokens for the iterator:
+sub END_ITERATOR { return 0; }
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
@@ -2175,11 +2681,6 @@ sub min {
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;
@@ -2235,6 +2736,9 @@ sub new {
# that isn't just a redirector.
my $resource; my $resourceCount = 0;
+ # Documentation on this algorithm can be found in the CVS repository at
+ # /docs/lonnavdocs; these "**#**" markers correspond to documentation
+ # in that file.
# **1**
foreach my $pass (@iterations) {
@@ -2250,13 +2754,13 @@ sub new {
# prime the recursion
$self->{$firstResourceName}->{DATA}->{$valName} = 0;
- my $depth = 0;
- $iterator->next();
+ $iterator->next();
my $curRes = $iterator->next();
- while ($depth > -1) {
- if ($curRes == $iterator->BEGIN_MAP()) { $depth++; }
- if ($curRes == $iterator->END_MAP()) { $depth--; }
-
+ my $depth = 1;
+ while ($depth > 0) {
+ 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,
@@ -2290,13 +2794,13 @@ sub new {
$curRes->{DATA}->{DISPLAY_DEPTH} = $finalDepth;
if ($finalDepth > $maxDepth) {$maxDepth = $finalDepth;}
}
- } continue {
- $curRes = $iterator->next();
+
+ $curRes = $iterator->next();
}
}
# Check: Was this only one resource, a map?
- if ($resourceCount == 1 && $resource->is_map() && !$self->{FORCE_TOP}) {
+ if ($resourceCount == 1 && $resource->is_sequence() && !$self->{FORCE_TOP}) {
my $firstResource = $resource->map_start();
my $finishResource = $resource->map_finish();
return
@@ -2312,6 +2816,7 @@ sub new {
$self->{MAX_DEPTH} = $maxDepth;
$self->{STACK} = [];
$self->{RECURSIVE_ITERATOR_FLAG} = 0;
+ $self->{FINISHED} = 0; # When true, the iterator has finished
for (my $i = 0; $i <= $self->{MAX_DEPTH}; $i++) {
push @{$self->{STACK}}, [];
@@ -2328,6 +2833,10 @@ sub new {
sub next {
my $self = shift;
+ my $closeAllPages=shift;
+ if ($self->{FINISHED}) {
+ return END_ITERATOR();
+ }
# If we want to return the top-level map object, and haven't yet,
# do so.
@@ -2338,7 +2847,7 @@ sub next {
if ($self->{RECURSIVE_ITERATOR_FLAG}) {
# grab the next from the recursive iterator
- my $next = $self->{RECURSIVE_ITERATOR}->next();
+ my $next = $self->{RECURSIVE_ITERATOR}->next($closeAllPages);
# is it a begin or end map? If so, update the depth
if ($next == BEGIN_MAP() ) { $self->{RECURSIVE_DEPTH}++; }
@@ -2388,6 +2897,7 @@ sub next {
$self->{CURRENT_DEPTH}--;
return END_BRANCH();
} else {
+ $self->{FINISHED} = 1;
return END_MAP();
}
}
@@ -2451,7 +2961,7 @@ sub next {
# That ends the main iterator logic. Now, do we want to recurse
# down this map (if this resource is a map)?
- if ($self->{HERE}->is_map() &&
+ if ( ($self->{HERE}->is_sequence() || (!$closeAllPages && $self->{HERE}->is_page())) &&
(defined($self->{FILTER}->{$self->{HERE}->map_pc()}) xor $self->{CONDITION})) {
$self->{RECURSIVE_ITERATOR_FLAG} = 1;
my $firstResource = $self->{HERE}->map_start();
@@ -2469,7 +2979,7 @@ sub next {
my $browsePriv = $self->{HERE}->browsePriv();
if (!$self->{HERE}->src() ||
(!($browsePriv eq 'F') && !($browsePriv eq '2')) ) {
- return $self->next();
+ return $self->next($closeAllPages);
}
return $self->{HERE};
@@ -2521,7 +3031,7 @@ package Apache::lonnavmaps::DFSiterator;
# 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
+# but this might as well be left separate, 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.
@@ -2700,8 +3210,9 @@ use Apache::lonnet;
=pod
-=head1 Object: resource
+=head1 Object: resource
+X
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
@@ -2721,24 +3232,52 @@ writing, there is no way to override thi
parts will never be returned, nor will their response types or ids be
stored.
-=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 Overview
-=head2 Methods
-
-=over 4
-
-=item * B($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
+A B is the most granular type of object in LON-CAPA that can
+be included in a course. It can either be a particular resource, like
+an HTML page, external resource, problem, etc., or it can be a
+container sequence, such as a "page" or a "map".
+
+To see a sequence from the user's point of view, please see the
+B chapter of the Author's
+Manual.
+
+A Resource Object, once obtained from a navmap object via a B
+method of the navmap, or from an iterator, allows you to query
+information about that resource.
+
+Generally, you do not ever want to create a resource object yourself,
+so creation has been left undocumented. Always retrieve resources
+from navmap objects.
+
+=head3 Identifying Resources
+
+XEvery resource is identified by a Resource ID in the big hash that is
+unique to that resource for a given course. X
+The Resource ID has the form #.#, where the first number is the same
+for every resource in a map, and the second is unique. For instance,
+for a course laid out like this:
+
+ * Problem 1
+ * Map
+ * Resource 2
+ * Resource 3
+
+C and C will share a first number, and C
+C will share a first number. The second number may end up
+re-used between the two groups.
+
+The resource ID is only used in the big hash, but can be used in the
+context of a course to identify a resource easily. (For instance, the
+printing system uses it to record which resources from a sequence you
+wish to print.)
+
+X X
+All resources also have Bs, which uniquely identify a resource
+in a course. Many internal LON-CAPA functions expect a symb. A symb
+carries along with it the URL of the resource, and the map it appears
+in. Symbs are much larger then resource IDs.
=cut
@@ -2777,14 +3316,21 @@ sub navHash {
=pod
-B
+=head2 Methods
+
+Once you have a resource object, here's what you can do with it:
-These are methods that help you retrieve metadata about the resource:
-Method names are based on the fields in the compiled course
-representation.
+=head3 Attribute Retrieval
+
+Every resource has certain attributes that can be retrieved and used:
=over 4
+=item * B: Every resource has an ID that is unique for that
+ resource in the course it is in. The ID is actually in the hash
+ representing the resource, so for a resource object $res, obtain
+ it via C<$res->{ID}).
+
=item * B:
Returns a "composite title", that is equal to $res->title() if the
@@ -2795,11 +3341,6 @@ resource has a title, and is otherwise t
Returns true if the resource is external.
-=item * B:
-
-Returns the "goesto" value from the compiled nav map. (It is likely
-you want to use B instead.)
-
=item * B:
Returns the kind of the resource from the compiled nav map.
@@ -2827,11 +3368,6 @@ Returns the symb for the resource.
Returns the title of the resource.
-=item * B:
-
-Returns the "to" value from the compiled nav map. (It is likely you
-want to use B instead.)
-
=back
=cut
@@ -2841,6 +3377,7 @@ want to use B instead.)
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); }
+# considered private and undocumented
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); }
@@ -2857,11 +3394,19 @@ 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))
+ my $symb = &Apache::lonnet::declutter($self->navHash('map_id_'.$first))
. '___' . $second . '___' . $symbSrc;
+ return &Apache::lonnet::symbclean($symb);
}
-sub title { my $self=shift; return $self->navHash("title_", 1); }
+sub title {
+ my $self=shift;
+ if ($self->{ID} eq '0.0') {
+ # If this is the top-level map, return the title of the course
+ # since this map can not be titled otherwise.
+ return $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
+ }
+ return $self->navHash("title_", 1); }
+# considered private and undocumented
sub to { my $self=shift; return $self->navHash("to_", 1); }
sub compTitle {
my $self = shift;
@@ -2903,6 +3448,15 @@ Returns true if the resource is a sequen
=cut
+sub hasResource {
+ my $self = shift;
+ return $self->{NAV_MAP}->hasResource(@_);
+}
+
+sub retrieveResources {
+ my $self = shift;
+ return $self->{NAV_MAP}->retrieveResources(@_);
+}
sub is_html {
my $self=shift;
@@ -2913,17 +3467,44 @@ sub is_map { my $self=shift; return defi
sub is_page {
my $self=shift;
my $src = $self->src();
- return ($src =~ /page$/);
+ return $self->navHash("is_map_", 1) &&
+ $self->navHash("map_type_" . $self->map_pc()) eq 'page';
}
sub is_problem {
my $self=shift;
my $src = $self->src();
- return ($src =~ /problem$/);
+ return ($src =~ /\.(problem|exam|quiz|assess|survey|form|library)$/)
+}
+sub contains_problem {
+ my $self=shift;
+ if ($self->is_page()) {
+ my $hasProblem=$self->hasResource($self,sub { $_[0]->is_problem() },1);
+ return $hasProblem;
+ }
+ return 0;
}
sub is_sequence {
my $self=shift;
my $src = $self->src();
- return ($src =~ /sequence$/);
+ return $self->navHash("is_map_", 1) &&
+ $self->navHash("map_type_" . $self->map_pc()) eq 'sequence';
+}
+sub is_survey {
+ my $self = shift();
+ my $part = shift();
+ if ($self->parmval('type',$part) eq 'survey') {
+ return 1;
+ }
+ if ($self->src() =~ /\.(survey)$/) {
+ return 1;
+ }
+ return 0;
+}
+
+sub is_empty_sequence {
+ my $self=shift;
+ my $src = $self->src();
+ return !$self->is_page() && $self->navHash("is_map_", 1) && !$self->navHash("map_type_" . $self->map_pc());
}
# Private method: Shells out to the parmval in the nav map, handler parts.
@@ -2998,8 +3579,6 @@ sub map_type {
return $self->navHash("map_type_$pc", 0);
}
-
-
#####
# Property queries
#####
@@ -3037,6 +3616,11 @@ Get the Client IP/Name Access Control in
Get the answer-reveal date for the problem.
+=item * B:
+
+Gets the awarded value for the problem part. Requires genUserData set to
+true when the navmap object was created.
+
=item * B:
Get the due date for the problem.
@@ -3090,9 +3674,19 @@ sub answerdate {
}
return $self->parmval("answerdate", $part);
}
-sub awarded { my $self = shift; return $self->queryRestoreHash('awarded', shift); }
+sub awarded {
+ my $self = shift; my $part = shift;
+ $self->{NAV_MAP}->get_user_data();
+ if (!defined($part)) { $part = '0'; }
+ return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.awarded'};
+}
sub duedate {
(my $self, my $part) = @_;
+ my $interval=$self->parmval("interval", $part);
+ if ($interval) {
+ my $first_access=&Apache::lonnet::get_first_access('map',$self->symb);
+ if ($first_access) { return ($first_access+$interval); }
+ }
return $self->parmval("duedate", $part);
}
sub maxtries {
@@ -3109,7 +3703,7 @@ sub opendate {
}
sub problemstatus {
(my $self, my $part) = @_;
- return $self->parmval("problemstatus", $part);
+ return lc $self->parmval("problemstatus", $part);
}
sub sig {
(my $self, my $part) = @_;
@@ -3131,7 +3725,21 @@ sub type {
}
sub weight {
my $self = shift; my $part = shift;
- return $self->parmval("weight", $part);
+ if (!defined($part)) { $part = '0'; }
+ return &Apache::lonnet::EXT('resource.'.$part.'.weight',
+ $self->symb(), $ENV{'user.domain'},
+ $ENV{'user.name'},
+ $ENV{'request.course.sec'});
+}
+sub part_display {
+ my $self= shift(); my $partID = shift();
+ if (! defined($partID)) { $partID = '0'; }
+ my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',
+ $self->symb);
+ if (! defined($display) || $display eq '') {
+ $display = $partID;
+ }
+ return $display;
}
# Multiple things need this
@@ -3272,12 +3880,21 @@ sub multipart {
return $self->countParts() > 1;
}
+sub singlepart {
+ my $self = shift;
+ return $self->countParts() == 1;
+}
+
sub responseType {
my $self = shift;
my $part = shift;
$self->extractParts();
- return $self->{RESPONSE_TYPE}->{$part};
+ if (defined($self->{RESPONSE_TYPES}->{$part})) {
+ return @{$self->{RESPONSE_TYPES}->{$part}};
+ } else {
+ return undef;
+ }
}
sub responseIds {
@@ -3285,7 +3902,11 @@ sub responseIds {
my $part = shift;
$self->extractParts();
- return $self->{RESPONSE_IDS}->{$part};
+ if (defined($self->{RESPONSE_IDS}->{$part})) {
+ return @{$self->{RESPONSE_IDS}->{$part}};
+ } else {
+ return undef;
+ }
}
# Private function: Extracts the parts information, both part names and
@@ -3302,32 +3923,44 @@ sub extractParts {
# Retrieve part count, if this is a problem
if ($self->is_problem()) {
+ my $partorder = &Apache::lonnet::metadata($self->src(), 'partorder');
my $metadata = &Apache::lonnet::metadata($self->src(), 'packages');
- if (!$metadata) {
- $self->{RESOURCE_ERROR} = 1;
- $self->{PARTS} = [];
- $self->{PART_TYPE} = {};
- return;
- }
- foreach (split(/\,/,$metadata)) {
- if ($_ =~ /^part_(.*)$/) {
- my $part = $1;
- # This floods the logs if it blows up
- if (defined($parts{$part})) {
- Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->symb());
- }
-
- # check to see if part is turned off.
- if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) {
- $parts{$part} = 1;
- }
- }
+ if ($partorder) {
+ my @parts;
+ for my $part (split (/,/,$partorder)) {
+ if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) {
+ push @parts, $part;
+ $parts{$part} = 1;
+ }
+ }
+ $self->{PARTS} = \@parts;
+ } else {
+ if (!$metadata) {
+ $self->{RESOURCE_ERROR} = 1;
+ $self->{PARTS} = [];
+ $self->{PART_TYPE} = {};
+ return;
+ }
+ foreach (split(/\,/,$metadata)) {
+ if ($_ =~ /^part_(.*)$/) {
+ my $part = $1;
+ # This floods the logs if it blows up
+ if (defined($parts{$part})) {
+ &Apache::lonnet::logthis("$part multiply defined in metadata for " . $self->symb());
+ }
+
+ # check to see if part is turned off.
+
+ if (!Apache::loncommon::check_if_partid_hidden($part, $self->symb())) {
+ $parts{$part} = 1;
+ }
+ }
+ }
+ my @sortedParts = sort keys %parts;
+ $self->{PARTS} = \@sortedParts;
}
-
- my @sortedParts = sort keys %parts;
- $self->{PARTS} = \@sortedParts;
my %responseIdHash;
my %responseTypeHash;
@@ -3339,7 +3972,7 @@ sub extractParts {
}
# Now, the unfortunate thing about this is that parts, part name, and
- # response if are delimited by underscores, but both the part
+ # response id are delimited by underscores, but both the part
# name and response id can themselves have underscores in them.
# So we have to use our knowlege of part names to figure out
# where the part names begin and end, and even then, it is possible
@@ -3351,7 +3984,6 @@ sub extractParts {
my $partIdSoFar = '';
my @partChunks = split /_/, $partStuff;
my $i = 0;
-
for ($i = 0; $i < scalar(@partChunks); $i++) {
if ($partIdSoFar) { $partIdSoFar .= '_'; }
$partIdSoFar .= $partChunks[$i];
@@ -3359,13 +3991,25 @@ sub extractParts {
my @otherChunks = @partChunks[$i+1..$#partChunks];
my $responseId = join('_', @otherChunks);
push @{$responseIdHash{$partIdSoFar}}, $responseId;
- $responseTypeHash{$partIdSoFar} = $responseType;
- last;
+ push @{$responseTypeHash{$partIdSoFar}}, $responseType;
}
}
}
}
-
+ my $resorder = &Apache::lonnet::metadata($self->src(),'responseorder');
+ if ($resorder) {
+ my @resorder=split(/,/,$resorder);
+ foreach my $part (keys(%responseIdHash)) {
+ my %resids = map { ($_,1) } @{ $responseIdHash{$part} };
+ my @neworder;
+ foreach my $possibleid (@resorder) {
+ if (exists($resids{$possibleid})) {
+ push(@neworder,$possibleid);
+ }
+ }
+ $responseIdHash{$part}=\@neworder;
+ }
+ }
$self->{RESPONSE_IDS} = \%responseIdHash;
$self->{RESPONSE_TYPES} = \%responseTypeHash;
}
@@ -3557,7 +4201,7 @@ sub getCompletionStatus {
my $status = $self->queryRestoreHash('solved', shift);
- # Left as seperate if statements in case we ever do more with this
+ # Left as separate 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; }
@@ -3675,9 +4319,15 @@ sub status {
# dimension and 5 entries on the other, which we want to colorize,
# plus network failure and "no date data at all".
+ #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; }
if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; }
- my $suppressFeedback = lc($self->parmval("problemstatus", $part)) eq 'no';
+ my $suppressFeedback = $self->problemstatus($part) eq 'no';
+ # If there's an answer date and we're past it, don't
+ # suppress the feedback; student should know
+ if ($self->answerdate($part) && $self->answerdate($part) < time()) {
+ $suppressFeedback = 0;
+ }
# There are a few whole rows we can dispose of:
if ($completionStatus == CORRECT ||
@@ -3703,7 +4353,7 @@ sub status {
if ($dateStatus == PAST_DUE_ANSWER_LATER ||
$dateStatus == PAST_DUE_NO_ANSWER ) {
- return $dateStatus;
+ return $suppressFeedback ? ANSWER_SUBMITTED : $dateStatus;
}
if ($dateStatus == ANSWER_OPEN) {
@@ -3721,7 +4371,7 @@ sub status {
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 $suppressFeedback ? ANSWER_SUBMITTED : TRIES_LEFT;
}
return $suppressFeedback ? ANSWER_SUBMITTED : INCORRECT; # otherwise, return orange; student can't fix this
}
@@ -3730,6 +4380,96 @@ sub status {
return OPEN;
}
+sub CLOSED { return 23; }
+sub ERROR { return 24; }
+
+=pod
+
+B
+
+Convenience method B provides a "simple status" for the resource.
+"Simple status" corresponds to "which icon is shown on the
+Navmaps". There are six "simple" statuses:
+
+=over 4
+
+=item * B: The problem is currently closed. (No icon shown.)
+
+=item * B: The problem is open and unattempted.
+
+=item * B: The problem is correct for any reason.
+
+=item * B: The problem is incorrect and can still be
+completed successfully.
+
+=item * B: The problem has been attempted, but the student
+does not know if they are correct. (The ellipsis icon.)
+
+=item * B: There is an error retrieving information about this
+problem.
+
+=back
+
+=cut
+
+# This hash maps the composite status to this simple status, and
+# can be used directly, if you like
+my %compositeToSimple =
+ (
+ NETWORK_FAILURE() => ERROR,
+ NOTHING_SET() => CLOSED,
+ CORRECT() => CORRECT,
+ EXCUSED() => CORRECT,
+ PAST_DUE_NO_ANSWER() => INCORRECT,
+ PAST_DUE_ANSWER_LATER() => INCORRECT,
+ ANSWER_OPEN() => INCORRECT,
+ OPEN_LATER() => CLOSED,
+ TRIES_LEFT() => OPEN,
+ INCORRECT() => INCORRECT,
+ OPEN() => OPEN,
+ ATTEMPTED() => ATTEMPTED,
+ ANSWER_SUBMITTED() => ATTEMPTED
+ );
+
+sub simpleStatus {
+ my $self = shift;
+ my $part = shift;
+ my $status = $self->status($part);
+ return $compositeToSimple{$status};
+}
+
+=pod
+
+B will return an array reference containing, in
+this order, the number of OPEN, CLOSED, CORRECT, INCORRECT, ATTEMPTED,
+and ERROR parts the given problem has.
+
+=cut
+
+# This maps the status to the slot we want to increment
+my %statusToSlotMap =
+ (
+ OPEN() => 0,
+ CLOSED() => 1,
+ CORRECT() => 2,
+ INCORRECT() => 3,
+ ATTEMPTED() => 4,
+ ERROR() => 5
+ );
+
+sub statusToSlot { return $statusToSlotMap{shift()}; }
+
+sub simpleStatusCount {
+ my $self = shift;
+
+ my @counts = (0, 0, 0, 0, 0, 0, 0);
+ foreach my $part (@{$self->parts()}) {
+ $counts[$statusToSlotMap{$self->simpleStatus($part)}]++;
+ }
+
+ return \@counts;
+}
+
=pod
B
@@ -3761,15 +4501,18 @@ sub completable {
# "If any of the parts are open, or have tries left (implies open),
# and it is not "attempted" (manually graded problem), it is
# not "complete"
- if (!(($status == OPEN() || $status == TRIES_LEFT())
- && $self->getCompletionStatus($part) != ATTEMPTED()
- && $status != ANSWER_SUBMITTED())) {
- return 0;
- }
+ if ($self->getCompletionStatus($part) == ATTEMPTED() ||
+ $status == ANSWER_SUBMITTED() ) {
+ # did this part already, as well as we can
+ next;
+ }
+ if ($status == OPEN() || $status == TRIES_LEFT()) {
+ return 1;
+ }
}
# If all the parts were complete, so was this problem.
- return 1;
+ return 0;
}
=pod