--- loncom/interface/lonnavmaps.pm 2007/08/29 00:55:37 1.400 +++ loncom/interface/lonnavmaps.pm 2012/06/19 10:11:35 1.487 @@ -1,7 +1,8 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.400 2007/08/29 00:55:37 albertel Exp $ +# $Id: lonnavmaps.pm,v 1.487 2012/06/19 10:11:35 foxr Exp $ + # # Copyright Michigan State University Board of Trustees # @@ -27,389 +28,25 @@ # ### -package Apache::lonnavmaps; - -use strict; -use GDBM_File; -use Apache::loncommon(); -use Apache::lonenc(); -use Apache::lonlocal; -use Apache::lonnet; -use POSIX qw (floor strftime); -use Data::Dumper; # for debugging, not always -use Time::HiRes qw( gettimeofday tv_interval ); -use LONCAPA; - -# 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->CLOSED => '', - $resObj->OPEN => 'navmap.open.gif', - $resObj->CORRECT => 'navmap.correct.gif', - $resObj->PARTIALLY_CORRECT => 'navmap.partial.gif', - $resObj->INCORRECT => 'navmap.wrong.gif', - $resObj->ATTEMPTED => 'navmap.ellipsis.gif', - $resObj->ERROR => '' - ); - -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 => '', - $resObj->ATTEMPTED => '', - $resObj->ANSWER_SUBMITTED => '', - $resObj->PARTIALLY_CORRECT => '#006600' - ); -# And a special case in the nav map; what to do when the assignment -# is not yet done and due in less than 24 hours -my $hurryUpColor = "#FF0000"; - -sub close { - if ($env{'environment.remotenavmap'} ne 'on') { return ''; } - return(< -window.status='Accessing Nav Control'; -menu=window.open("/adm/rat/empty.html","loncapanav", - "height=600,width=400,scrollbars=1"); -window.status='Closing Nav Control'; -menu.close(); -window.status='Done.'; - -ENDCLOSE -} - -sub update { - if ($env{'environment.remotenavmap'} ne 'on') { return ''; } - if (!$env{'request.course.id'}) { return ''; } - if ($ENV{'REQUEST_URI'}=~m|^/adm/navmaps|) { return ''; } - return(< - -ENDUPDATE -} - -# 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. -sub getLinkForResource { - my $stack = shift; - my $res; - - # Check to see if there are any pages in the stack - foreach $res (@$stack) { - if (defined($res)) { - my $anchor; - if ($res->is_page()) { - foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } - $anchor=&escape($anchor->shown_symb()); - return ($res->link(),$res->shown_symb(),$anchor); - } - # in case folder was skipped over as "only sequence" - my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); - if ($map=~/\.page$/) { - my $url=&Apache::lonnet::clutter($map); - $anchor=&escape($src->shown_symb()); - return ($url,$res->shown_symb(),$anchor); - } - } - } - - # 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 my $item (@$stack) { - if (defined($item)) { $res = $item; } - } - - return ($res->link(),$res->shown_symb()); -} - -# Convenience function: This separates the logic of how to create -# the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", -# etc.) into a separate 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 &mt("Having technical difficulties; please check status later"); - } - if ($status == $res->NOTHING_SET) { - return &mt("Not currently assigned."); - } - if ($status == $res->OPEN_LATER) { - return "Open " . timeToHumanString($res->opendate($part),'start'); - } - if ($status == $res->OPEN) { - if ($res->duedate($part)) { - return &mt("Due")." " .timeToHumanString($res->duedate($part),'end'); - } else { - return &mt("Open, no due date"); - } - } - if ($status == $res->PAST_DUE_ANSWER_LATER) { - return &mt("Answer open")." " . timeToHumanString($res->answerdate($part),'start'); - } - if ($status == $res->PAST_DUE_NO_ANSWER) { - return &mt("Was due")." " . timeToHumanString($res->duedate($part),'end'); - } - if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) - && $res->handgrade($part) ne 'yes') { - return &mt("Answer available"); - } - if ($status == $res->EXCUSED) { - return &mt("Excused by instructor"); - } - if ($status == $res->ATTEMPTED) { - return &mt("Answer submitted, not yet graded"); - } - if ($status == $res->TRIES_LEFT) { - my $tries = $res->tries($part); - my $maxtries = $res->maxtries($part); - my $triesString = ""; - if ($tries && $maxtries) { - $triesString = "($tries of $maxtries tries used)"; - if ($maxtries > 1 && $maxtries - $tries == 1) { - $triesString = "$triesString"; - } - } - if ($res->duedate($part)) { - return &mt("Due")." " . timeToHumanString($res->duedate($part),'end') . - " $triesString"; - } else { - return &mt("No due date")." $triesString"; - } - } - if ($status == $res->ANSWER_SUBMITTED) { - return &mt('Answer submitted'); - } -} - -# Convenience function, so others can use it: Is the problem due in less than -# 24 hours, and still can be done? - -sub dueInLessThan24Hours { - my $res = shift; - my $part = shift; - my $status = $res->status($part); - - return ($status == $res->OPEN() || - $status == $res->TRIES_LEFT()) && - $res->duedate($part) && $res->duedate($part) < time()+(24*60*60) && - $res->duedate($part) > time(); -} - -# Convenience function, so others can use it: Is there only one try remaining for the -# part, with more than 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($part) && - $res->duedate($part) > 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,$type,$format) = @_; - - # zero, '0' and blank are bad times - if (!$time) { - return &mt('never'); - } - unless (&Apache::lonlocal::current_language()=~/^en/) { - return &Apache::lonlocal::locallocaltime($time); - } - 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 than a minute - if ( $delta < $minute ) { - if ($delta == 1) { return "${prefix}1 second$tense"; } - return "$prefix$delta seconds$tense"; - } - - # Less than 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 than 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"; - } - - # If there's a caller supplied format, use it. - - if($format ne '') { - my $timeStr = strftime($format, localtime($time)); - return $timeStr.&Apache::lonlocal::gettimezone($time); - } - - # Less than 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/00:00/; - $timeStr =~ s/12:00 pm/noon/; - return ($inPast ? "last " : "this ") . - $timeStr.&Apache::lonlocal::gettimezone($time); - } - - my $conjunction='on'; - if ($type eq 'start') { - $conjunction='at'; - } elsif ($type eq 'end') { - $conjunction='by'; - } - # Is it this year? - if ( $time[5] == $now[5]) { - # Return on Month Day, HH:MM meridian - my $timeStr = strftime("$conjunction %A, %b %e at %I:%M %P", localtime($time)); - $timeStr =~ s/12:00 am/00:00/; - $timeStr =~ s/12:00 pm/noon/; - return $timeStr.&Apache::lonlocal::gettimezone($time); - } - - # Not this year, so show the year - my $timeStr = strftime("$conjunction %A, %b %e %Y at %I:%M %P", localtime($time)); - $timeStr =~ s/12:00 am/00:00/; - $timeStr =~ s/12:00 pm/noon/; - return $timeStr.&Apache::lonlocal::gettimezone($time); - } -} - - =pod =head1 NAME -Apache::lonnavmap - Subroutines to handle and render the navigation - maps +Apache::lonnavmaps - Subroutines to handle and render the navigation =head1 SYNOPSIS +Handles navigational maps. + The main handler generates the navigational listing for the course, the other objects export this information in a usable fashion for other modules. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org. + + =head1 OVERVIEW X When a user enters a course, LON-CAPA examines the @@ -604,6 +241,11 @@ instruct the renderer to render only a p the source of the map you want to process, like '/res/103/jerf/navmap.course.sequence'. +=item * B: default: false + +If you need to include the top level map (meaning the course) in the +rendered output set this to true + =item * B: default: constructs one from %env A reference to a navmap, used only if an iterator is not passed in. If @@ -724,6 +366,528 @@ navmaps screen) uses this to display the =cut + +=head1 SUBROUTINES + +=over + +=item update() + +=item addToFilter() + +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 + +=item removeFromFilter() + +=item getLinkForResource() + +Convenience function: Given a stack returned from getStack on the iterator, +return the correct src() value. + +=item getDescription() + +Convenience function: This separates the logic of how to create +the problem text strings ("Due: DATE", "Open: DATE", "Not yet assigned", +etc.) into a separate 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. + +=item dueInLessThan24Hours() + +Convenience function, so others can use it: Is the problem due in less than 24 hours, and still can be done? + +=item lastTry() + +Convenience function, so others can use it: Is there only one try remaining for the +part, with more than one try to begin with, not due yet and still can be done? + +=item advancedUser() + +This puts a human-readable name on the env variable. + +=item timeToHumanString() + +timeToHumanString takes a time number and converts it to a +human-readable representation, meant to be used in the following +manner: + +=over 4 + +=item * print "Due $timestring" + +=item * print "Open $timestring" + +=item * print "Answer available $timestring" + +=back + +Very, very, very, VERY English-only... goodness help a localizer on +this func... + +=item resource() + +returns 0 + +=item communication_status() + +returns 1 + +=item quick_status() + +returns 2 + +=item long_status() + +returns 3 + +=item part_status_summary() + +returns 4 + +=item render_resource() + +=item render_communication_status() + +=item render_quick_status() + +=item render_long_status() + +=item render_parts_summary_status() + +=item setDefault() + +=item cmp_title() + +=item render() + +=item add_linkitem() + +=item show_linkitems_toolbar() + +=back + +=cut + +package Apache::lonnavmaps; + +use strict; +use GDBM_File; +use Apache::loncommon(); +use Apache::lonenc(); +use Apache::lonlocal; +use Apache::lonnet; +use Apache::lonmap; + +use POSIX qw (floor strftime); +use Time::HiRes qw( gettimeofday tv_interval ); +use LONCAPA; +use DateTime(); +use HTML::Entities; + +# For debugging + +use Data::Dumper; + + +# 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 usesthe colors +# instead of the icons. +my %statusIconMap = + ( + $resObj->CLOSED => '', + $resObj->OPEN => 'navmap.open.gif', + $resObj->CORRECT => 'navmap.correct.gif', + $resObj->PARTIALLY_CORRECT => 'navmap.partial.gif', + $resObj->INCORRECT => 'navmap.wrong.gif', + $resObj->ATTEMPTED => 'navmap.ellipsis.gif', + $resObj->ERROR => '' + ); + +my %iconAltTags = #texthash does not work here + ( 'navmap.correct.gif' => 'Correct', + 'navmap.wrong.gif' => 'Incorrect', + 'navmap.open.gif' => 'Is Open', + 'navmap.partial.gif' => 'Partially Correct', + 'navmap.ellipsis.gif' => 'Attempted', + ); + +# 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 => '', + $resObj->ATTEMPTED => '', + $resObj->CREDIT_ATTEMPTED => '', + $resObj->ANSWER_SUBMITTED => '', + $resObj->PARTIALLY_CORRECT => '#006600' + ); +# And a special case in the nav map; what to do when the assignment +# is not yet done and due in less than 24 hours +my $hurryUpColor = "#FF0000"; + +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)); +} + +sub getLinkForResource { + my $stack = shift; + my $res; + + # Check to see if there are any pages in the stack + foreach $res (@$stack) { + if (defined($res)) { + my $anchor; + if ($res->is_page()) { + foreach my $item (@$stack) { if (defined($item)) { $anchor = $item; } } + $anchor=&escape($anchor->shown_symb()); + return ($res->link(),$res->shown_symb(),$anchor); + } + # in case folder was skipped over as "only sequence" + my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); + if ($map=~/\.page$/) { + my $url=&Apache::lonnet::clutter($map); + $anchor=&escape($res->shown_symb()); + return ($url,$res->shown_symb(),$anchor); + } + } + } + + # 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 my $item (@$stack) { + if (defined($item)) { $res = $item; } + } + + if ($res) { + return ($res->link(),$res->shown_symb()); + } + return; +} + + + +sub getDescription { + my $res = shift; + my $part = shift; + my $status = $res->status($part); + + my $open = $res->opendate($part); + my $due = $res->duedate($part); + my $answer = $res->answerdate($part); + + if ($status == $res->NETWORK_FAILURE) { + return &mt("Having technical difficulties; please check status later"); + } + if ($status == $res->NOTHING_SET) { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("Not currently assigned.",$res->symb(),'opendate'),$part); + } + if ($status == $res->OPEN_LATER) { + return &mt("Open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($open,'start'),$res->symb(),'opendate',$part)); + } + if ($res->simpleStatus($part) == $res->OPEN) { + unless (&Apache::lonnet::allowed('mgr',$env{'request.course.id'})) { + my ($slot_status,$slot_time,$slot_name)=$res->check_for_slot($part); + if ($slot_status == $res->UNKNOWN) { + return &mt('Reservation status unknown'); + } elsif ($slot_status == $res->RESERVED) { + return &mt('Reserved - ends [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVED_LOCATION) { + return &mt('Reserved - specific location(s) - ends [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVED_LATER) { + return &mt('Reserved - next open [_1]', + timeToHumanString($slot_time,'start')); + } elsif ($slot_status == $res->RESERVABLE) { + return &mt('Reservable, reservations close [_1]', + timeToHumanString($slot_time,'end')); + } elsif ($slot_status == $res->RESERVABLE_LATER) { + return &mt('Reservable, reservations open [_1]', + timeToHumanString($slot_time,'start')); + } elsif ($slot_status == $res->NOT_IN_A_SLOT) { + return &mt('Reserve a time/place to work'); + } elsif ($slot_status == $res->NOTRESERVABLE) { + return &mt('Reservation not available'); + } elsif ($slot_status == $res->WAITING_FOR_GRADE) { + return &mt('Submission in grading queue'); + } + } + } + if ($status == $res->OPEN) { + if ($due) { + if ($res->is_practice()) { + return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)); + } else { + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)); + } + } else { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("Open, no due date"),$res->symb(),'duedate',$part); + } + } + if ($status == $res->PAST_DUE_ANSWER_LATER) { + return &mt("Answer open [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($answer,'start'),$res->symb(),'answerdate',$part)); + } + if ($status == $res->PAST_DUE_NO_ANSWER) { + if ($res->is_practice()) { + return &mt("Closed [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'answerdate,duedate',$part)); + } else { + return &mt("Was due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'answerdate,duedate',$part)); + } + } + if (($status == $res->ANSWER_OPEN || $status == $res->PARTIALLY_CORRECT) + && $res->handgrade($part) ne 'yes') { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("Answer available"),$res->symb(),'answerdate,duedate',$part); + } + if ($status == $res->EXCUSED) { + return &mt("Excused by instructor"); + } + if ($status == $res->ATTEMPTED) { + if ($res->is_anonsurvey($part) || $res->is_survey($part)) { + return &mt("Survey submission recorded"); + } else { + return &mt("Answer submitted, not yet graded"); + } + } + if ($status == $res->CREDIT_ATTEMPTED) { + if ($res->is_anonsurvey($part) || $res->is_survey($part)) { + return &mt("Credit for survey submission"); + } + } + if ($status == $res->TRIES_LEFT) { + my $tries = $res->tries($part); + my $maxtries = $res->maxtries($part); + my $triesString = ""; + if ($tries && $maxtries) { + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; + if ($maxtries > 1 && $maxtries - $tries == 1) { + $triesString = "$triesString"; + } + } + if ($due) { + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)) . + " $triesString"; + } else { + return &Apache::lonhtmlcommon::direct_parm_link(&mt("No due date"),$res->symb(),'duedate',$part)." $triesString"; + } + } + if ($status == $res->ANSWER_SUBMITTED) { + return &mt('Answer submitted'); + } +} + + +sub dueInLessThan24Hours { + my $res = shift; + my $part = shift; + my $status = $res->status($part); + + return ($status == $res->OPEN() || + $status == $res->TRIES_LEFT()) && + $res->duedate($part) && $res->duedate($part) < time()+(24*60*60) && + $res->duedate($part) > time(); +} + + +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($part) && + $res->duedate($part) > time(); +} + + +sub advancedUser { + return $env{'request.role.adv'}; +} + +sub timeToHumanString { + my ($time,$type,$format) = @_; + + # zero, '0' and blank are bad times + if (!$time) { + return &mt('never'); + } + unless (&Apache::lonlocal::current_language()=~/^en/) { + return &Apache::lonlocal::locallocaltime($time); + } + my $now = time(); + + # 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 than a minute + if ( $delta < $minute ) { + if ($delta == 1) { return "${prefix}1 second$tense"; } + return "$prefix$delta seconds$tense"; + } + + # Less than an hour + if ( $delta < $hour ) { + # If so, use minutes; or minutes, seconds (if format requires) + my $minutes = floor($delta / 60); + if (($format ne '') && ($format =~ /\%(T|S)/)) { + my $display; + if ($minutes == 1) { + $display = "${prefix}1 minute"; + } else { + $display = "$prefix$minutes minutes"; + } + my $seconds = $delta % $minute; + if ($seconds == 0) { + $display .= $tense; + } elsif ($seconds == 1) { + $display .= ", 1 second$tense"; + } else { + $display .= ", $seconds seconds$tense"; + } + return $display; + } + if ($minutes == 1) { return "${prefix}1 minute$tense"; } + return "$prefix$minutes minutes$tense"; + } + + # Is it less than 24 hours away? If so, + # display hours + minutes, (and + seconds, if format specified it) + 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 = ""; + } + if (($format ne '') && ($format =~ /\%(T|S)/)) { + my $display = "$prefix$hourString$minuteString"; + my $seconds = $delta-(($hours * $hour)+($minutes * $minute)); + if ($seconds == 0) { + $display .= $tense; + } elsif ($seconds == 1) { + $display .= ", 1 second$tense"; + } else { + $display .= ", $seconds seconds$tense"; + } + return $display; + } + return "$prefix$hourString$minuteString$tense"; + } + + # Date/time is more than 24 hours away + + my $dt = DateTime->from_epoch(epoch => $time) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + + # If there's a caller supplied format, use it, unless it only displays + # H:M:S or H:M. + + if (($format ne '') && ($format ne '%T') && ($format ne '%R')) { + my $timeStr = $dt->strftime($format); + return $timeStr.' ('.$dt->time_zone_short_name().')'; + } + + # Less than 5 days away, display day of the week and + # HH:MM + + if ( $delta < $day * 5 ) { + my $timeStr = $dt->strftime("%A, %b %e at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return ($inPast ? "last " : "this ") . + $timeStr; + } + + my $conjunction='on'; + if ($type eq 'start') { + $conjunction='at'; + } elsif ($type eq 'end') { + $conjunction='by'; + } + # Is it this year? + my $dt_now = DateTime->from_epoch(epoch => $now) + ->set_time_zone(&Apache::lonlocal::gettimezone()); + if ( $dt->year() == $dt_now->year()) { + # Return on Month Day, HH:MM meridian + my $timeStr = $dt->strftime("$conjunction %A, %b %e at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return $timeStr; + } + + # Not this year, so show the year + my $timeStr = + $dt->strftime("$conjunction %A, %b %e %Y at %I:%M %P (%Z)"); + $timeStr =~ s/12:00 am/00:00/; + $timeStr =~ s/12:00 pm/noon/; + return $timeStr; + } +} + + sub resource { return 0; } sub communication_status { return 1; } sub quick_status { return 2; } @@ -733,16 +897,12 @@ sub part_status_summary { return 4; } sub render_resource { my ($resource, $part, $params) = @_; + my $editmapLink; my $nonLinkedText = ''; # stuff after resource title not in link my $link = $params->{"resourceLink"}; # The URL part is not escaped at this point, but the symb is... - # The stuff to the left of the ? must have ' replaced by \' since - # it will be quoted with ' in the href. - - my ($left,$right) = split(/\?/, $link); - $link = $left.'?'.$right; my $src = $resource->src(); my $it = $params->{"iterator"}; @@ -755,34 +915,32 @@ sub render_resource { my $location=&Apache::loncommon::lonhttpdurl("/adm/lonIcons"); # If this is a new branch, label it so if ($params->{'isNewBranch'}) { - $newBranchText = "Branch"; + $newBranchText = ".mt('Branch')."; } # links to open and close the folder - - my $linkopen = ""; - - + my $whitespace = $location.'/whitespace_21.gif'; + my $linkopen = "".""; my $linkclose = ""; # Default icon: unknown page - my $icon = ""; + my $icon = ""; if ($resource->is_problem()) { if ($part eq '0' || $params->{'condensed'}) { - $icon = ''.&mt('Task');
 	    } else {
 		$icon .= 'problem.gif'; + $icon .='" />'; } else { $icon = $params->{'indentString'}; } } else { - $icon = "  "; + $icon = ""; } # Display the correct map icon to open or shut map @@ -792,15 +950,15 @@ sub render_resource { if ($it->{CONDITION}) { $nowOpen = !$nowOpen; } - + my $folderType = $resource->is_sequence() ? 'folder' : 'page'; my $title=$resource->title; - $title=~s/\"/\"/g; + $title=~s/\"/\&qout;/g; if (!$params->{'resource_no_folder_link'}) { $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; - $icon = "\""."; - + $icon = "" + ."\"""; $linkopen = "{'url'} . '?' . $params->{'queryString'} . '&filter='; $linkopen .= ($nowOpen xor $it->{CONDITION}) ? @@ -815,26 +973,36 @@ sub render_resource { } else { # Don't allow users to manipulate folder - $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . - '.nomanip.gif'; - $icon = "\""."; + $icon = "navmap.$folderType." . ($nowOpen ? 'closed' : 'open') . '.gif'; + $icon = ""."\"".($nowOpen"; $linkopen = ""; $linkclose = ""; } + if ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) && + ($resource->symb=~/\_\_\_[^\_]+\_\_\_uploaded/)) { + my $icon = &Apache::loncommon::lonhttpdurl('/res/adm/pages').'/editmap.png'; + $editmapLink=' '. + ''. + ''.&mt('Edit Content').''. + ''; + } } if ($resource->randomout()) { - $nonLinkedText .= ' ('.&mt('hidden').') '; + $nonLinkedText .= ' ('.&mt('hidden').') '; } if (!$resource->condval()) { - $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; + $nonLinkedText .= ' ('.&mt('conditionally hidden').') '; + } + if (($resource->is_practice()) && ($resource->is_raw_problem())) { + $nonLinkedText .=' '.&mt('not graded').''; } - - # We're done preparing and finally ready to start the rendering - my $result = ""; + # We're done preparing and finally ready to start the rendering + my $result = ''; + my $newfolderType = $resource->is_sequence() ? 'folder' : 'page'; + my $indentLevel = $params->{'indentLevel'}; if ($newBranchText) { $indentLevel--; } @@ -844,7 +1012,6 @@ sub render_resource { } # Decide what to display - $result .= "$newBranchText$linkopen$icon$linkclose"; my $curMarkerBegin = ''; @@ -853,9 +1020,11 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = '>'; - $curMarkerEnd = '<'; - $params->{'displayedHereMarker'} = 1; + unless ($resource->is_map()) { + $curMarkerBegin = ''; + $curMarkerEnd = ''; + } + $params->{'displayedHereMarker'} = 1; } if ($resource->is_problem() && $part ne '0' && @@ -870,14 +1039,10 @@ sub render_resource { $nonLinkedText .= ' ('.&mt('[_1] parts', $resource->countParts()).')'; } - my $target; - if ($env{'environment.remotenavmap'} eq 'on') { - $target=' target="loncapaclient" '; - } if (!$params->{'resource_nolink'} && !$resource->is_sequence() && !$resource->is_empty_sequence) { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + $result .= "$curMarkerBegin$title$partLabel$curMarkerEnd$nonLinkedText"; } else { - $result .= " $curMarkerBegin$title$partLabel$curMarkerEnd $nonLinkedText"; + $result .= "$curMarkerBegin$linkopen$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; } return $result; @@ -888,16 +1053,13 @@ sub render_communication_status { my $discussionHTML = ""; my $feedbackHTML = ""; my $errorHTML = ""; my $link = $params->{"resourceLink"}; - my $target; - if ($env{'environment.remotenavmap'} eq 'on') { - $target=' target="loncapaclient" '; - } - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); + if ($resource->hasDiscussion()) { $discussionHTML = $linkopen . - ''.&mt('New Discussion').'' . + ''.&mt('New Discussion').'' . $linkclose; } @@ -905,10 +1067,9 @@ sub render_communication_status { my $feedback = $resource->getFeedback(); foreach my $msgid (split(/\,/, $feedback)) { if ($msgid) { - $feedbackHTML .= ' ' - . ''.&mt('New Email').''; + . ''.&mt('New E-mail').''; } } } @@ -920,10 +1081,9 @@ sub render_communication_status { last if ($errorcount>=10); # Only output 10 bombs maximum if ($msgid) { $errorcount++; - $errorHTML .= ' ' - . ''.&mt('New Error').''; + . ''.&mt('New Error').''; } } } @@ -931,8 +1091,7 @@ sub render_communication_status { if ($params->{'multipart'} && $part != '0') { $discussionHTML = $feedbackHTML = $errorHTML = ''; } - - return "$discussionHTML$feedbackHTML$errorHTML "; + return "$discussionHTML$feedbackHTML$errorHTML "; } sub render_quick_status { @@ -942,39 +1101,35 @@ sub render_quick_status { $params->{'multipart'} && $part eq "0"; my $link = $params->{"resourceLink"}; - my $target; - if ($env{'environment.remotenavmap'} eq 'on') { - $target=' target="loncapaclient" '; - } - my $linkopen = ""; + my $linkopen = ""; my $linkclose = ""; - + + $result .= ''; if ($resource->is_problem() && !$firstDisplayed) { - my $icon = $statusIconMap{$resource->simpleStatus($part)}; my $alt = $iconAltTags{$icon}; if ($icon) { my $location= &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); - $result .= "$linkopen$alt$linkclose\n"; + $result .= $linkopen.''.&mt($alt).''.$linkclose; } else { - $result .= " \n"; + $result .= " "; } } else { # not problem, no icon - $result .= " \n"; + $result .= " "; } - + $result .= "\n"; return $result; } sub render_long_status { my ($resource, $part, $params) = @_; - my $result = "\n"; + my $result = ''; my $firstDisplayed = !$params->{'condensed'} && $params->{'multipart'} && $part eq "0"; my $color; - if ($resource->is_problem()) { + if ($resource->is_problem() || $resource->is_practice()) { $color = $colormap{$resource->status}; if (dueInLessThan24Hours($resource, $part) || @@ -984,13 +1139,13 @@ sub render_long_status { } if ($resource->kind() eq "res" && - $resource->is_problem() && + $resource->is_raw_problem() && !$firstDisplayed) { if ($color) {$result .= ""; } $result .= getDescription($resource, $part); if ($color) {$result .= ""; } } - if ($resource->is_map() && advancedUser() && $resource->randompick()) { + if ($resource->is_map() && &advancedUser() && $resource->randompick()) { $result .= &mt('(randomly select [_1])', $resource->randompick()); } if ($resource->is_map() && &advancedUser() && $resource->randomorder()) { @@ -1029,7 +1184,6 @@ my %statusStrings = ); my @statuses = ($resObj->CORRECT, $resObj->ATTEMPTED, $resObj->INCORRECT, $resObj->OPEN, $resObj->CLOSED, $resObj->ERROR); -use Data::Dumper; sub render_parts_summary_status { my ($resource, $part, $params) = @_; if (!$resource->is_problem() && !$resource->contains_problem) { return ''; } @@ -1157,7 +1311,7 @@ sub render { if (!defined($navmap)) { $navmap = Apache::lonnavmaps::navmap->new(); if (!defined($navmap)) { - # no londer in course + # no longer in course return ''.&mt('No course selected').'
'.&mt('Select a course').'
'; } @@ -1225,6 +1379,11 @@ sub render { # Step 1: Check to see if we have a navmap if (!defined($navmap)) { $navmap = Apache::lonnavmaps::navmap->new(); + if (!defined($navmap)) { + # no longer in course + return ''.&mt('No course selected').'
+ '.&mt('Select a course').'
'; + } } # See if we're being passed a specific map @@ -1236,7 +1395,7 @@ sub render { $args->{'iterator'} = $it = $navmap->getIterator($firstResource, $finishResource, $filterHash, $condition); } else { - $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition); + $args->{'iterator'} = $it = $navmap->getIterator(undef, undef, $filterHash, $condition,undef,$args->{'include_top_level_map'}); } } @@ -1269,24 +1428,23 @@ sub render { my $printKey = $args->{'printKey'}; my $printCloseAll = $args->{'printCloseAll'}; if (!defined($printCloseAll)) { $printCloseAll = 1; } - + # Print key? if ($printKey) { $result .= ''; - my $date=localtime; $result.=''; my $location=&Apache::loncommon::lonhttpdurl("/adm/lonMisc"); if ($navmap->{LAST_CHECK}) { $result .= - ' '.&mt('New discussion since').' '. + ' '.&mt('New discussion since').' '. strftime("%A, %b %e at %I:%M %P", localtime($navmap->{LAST_CHECK})). ''; } else { $result .= ''; } @@ -1296,34 +1454,43 @@ sub render { if ($printCloseAll && !$args->{'resource_no_folder_link'}) { my ($link,$text); if ($condition) { - $link='"navmaps?condition=0&filter=&'.$queryString. - '&here='.&escape($here).'"'; + $link='navmaps?condition=0&filter=&'.$queryString. + '&here='.&escape($here); $text='Close all folders'; } else { - $link='"navmaps?condition=1&filter=&'.$queryString. - '&here='.&escape($here).'"'; + $link='navmaps?condition=1&filter=&'.$queryString. + '&here='.&escape($here); $text='Open all folders'; } + if ($env{'form.register'}) { + $link .= '&register='.$env{'form.register'}; + } if ($args->{'caller'} eq 'navmapsdisplay') { - &add_linkitem($args->{'linkitems'},'changefolder', - 'location.href='.$link,$text); + unless ($args->{'notools'}) { + &add_linkitem($args->{'linkitems'},'changefolder', + "location.href='$link'",$text); + } } else { - $result.=''.&mt($text).''; + $result.= ''.&mt($text).''; } $result .= "\n"; } # Check for any unread discussions in all resources. - if ($args->{'caller'} eq 'navmapsdisplay') { + if (($args->{'caller'} eq 'navmapsdisplay') && (!$args->{'notools'})) { &add_linkitem($args->{'linkitems'},'clearbubbles', 'document.clearbubbles.submit()', 'Mark all posts read'); my $time=time; + my $querystr = &HTML::Entities::encode($ENV{'QUERY_STRING'},'<>&"'); $result .= (< - + END + if ($env{'form.register'}) { + $result .= ''; + } if ($args->{'sort'} eq 'discussion') { my $totdisc = 0; my $haveDisc = ''; @@ -1344,55 +1511,51 @@ END } $result.=''; } + if (($args->{'caller'} eq 'navmapsdisplay') && + (&Apache::lonnet::allowed('mdc',$env{'request.course.id'}))) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + if ($env{'course.'.$env{'request.course.id'}.'.url'} eq + "uploaded/$cdom/$cnum/default.sequence") { + &add_linkitem($args->{'linkitems'},'edittoplevel', + "javascript:gocmd('/adm/coursedocs','editdocs');", + 'Content Editor'); + } + } if ($args->{'caller'} eq 'navmapsdisplay') { - $result .= '
Key:    '. - ' '.&mt('New message (click to open)').'

'. + ' '.&mt('New message (click to open)').'

'. '

  '. - ' '.&mt('Discussions').''. - '   '.&mt('New message (click to open)'). + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). '
'; - if ($env{'environment.remotenavmap'} ne 'on') { - $result .= ''; - } else { - $result .= ''; - } - $result.=&show_linkitems($args->{'linkitems'}); - if ($args->{'sort_html'}) { - if ($env{'environment.remotenavmap'} ne 'on') { - $result.=''. - ''; - } else { - $result.=''; - } - } - $result .= '
'. - &Apache::loncommon::help_open_menu('Navigation Screen','Navigation_Screen',undef,'RAT').' 
   '.$args->{'sort_html'}.'

'. - $args->{'sort_html'}.'
'; + $result .= &show_linkitems_toolbar($args,$condition); } elsif ($args->{'sort_html'}) { $result.=$args->{'sort_html'}; } - $result .= "
\n"; + #$result .= "
\n"; if ($r) { $r->print($result); $r->rflush(); $result = ""; } # End parameter setting - + + $result .= "
\n"; + # Data - $result .= '' ."\n"; + $result.=&Apache::loncommon::start_data_table("LC_tableOfContent"); + 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; - my $location= - &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace1.gif"); - $args->{'indentString'} = setDefault($args->{'indentString'}, "  "); + $args->{'condensed'} = 0; + + my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace_21.gif"); + $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; # If we're suppressing empty sequences, look for them here. Use DFS for speed, @@ -1611,16 +1774,15 @@ END if (defined($anchor)) { $anchor='#'.$anchor; } my $srcHasQuestion = $src =~ /\?/; $args->{"resourceLink"} = $src. - ($srcHasQuestion?'&':'?') . + ($srcHasQuestion?'&':'?') . 'symb=' . &escape($symb).$anchor; } # Now, we've decided what parts to show. Loop through them and # show them. foreach my $part (@parts) { $rownum ++; - my $backgroundColor = $backgroundColors[$rownum % scalar(@backgroundColors)]; - $result .= " \n"; + $result .= &Apache::loncommon::start_data_table_row(); # Set up some data about the parts that the cols might want my $filter = $it->{FILTER}; @@ -1646,7 +1808,7 @@ END } $result .= $colHTML . "\n"; } - $result .= " \n"; + $result .= &Apache::loncommon::end_data_table_row(); $args->{'isNewBranch'} = 0; } @@ -1665,6 +1827,8 @@ END } } } + + $result.=&Apache::loncommon::end_data_table(); # Print out the part that jumps to #curloc if it exists # delay needed because the browser is processing the jump before @@ -1674,16 +1838,13 @@ END # it's quite likely this might fix other browsers, too, and # certainly won't hurt anything. if ($displayedJumpMarker) { - $result .= " -"; +"); } - $result .= "
"; - if ($r) { $r->print($result); $result = ""; @@ -1699,42 +1860,75 @@ sub add_linkitem { $$linkitems{$name}{'text'}=&mt($text); } -sub show_linkitems { - my ($linkitems)=@_; - my @linkorder = ("blank","launchnav","closenav","firsthomework", - "everything","uncompleted","changefolder","clearbubbles"); - - my $result .= (< - -
-   -
'."\n"; - return $result; } 1; + + + + + + + + package Apache::lonnavmaps::navmap; =pod @@ -1811,6 +2005,12 @@ sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; + bless($self); # So we can call change_user if neceesary + + $self->{USERNAME} = shift || $env{'user.name'}; + $self->{DOMAIN} = shift || $env{'user.domain'}; + + # 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. @@ -1820,38 +2020,100 @@ sub new { # failed $self->{NETWORK_FAILURE} = 0; - # tie the nav hash + # We can only tie the nav hash as done below if the username/domain + # match the env one. Otherwise change_user does everything we need...since we can't + # assume there are course hashes for the specific requested user@domamin: + # - my %navmaphash; - my %parmhash; - my $courseFn = $env{"request.course.fn"}; - if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db", - &GDBM_READER(), 0640))) { - return undef; + if (($self->{USERNAME} eq $env{'user.name'}) && ($self->{DOMAIN} eq $env{'user.domain'})) { + + # tie the nav hash + + my %navmaphash; + my %parmhash; + my $courseFn = $env{"request.course.fn"}; + if (!(tie(%navmaphash, 'GDBM_File', "${courseFn}.db", + &GDBM_READER(), 0640))) { + return undef; + } + + if (!(tie(%parmhash, 'GDBM_File', "${courseFn}_parms.db", + &GDBM_READER(), 0640))) + { + untie %{$self->{PARM_HASH}}; + return undef; + } + + $self->{NAV_HASH} = \%navmaphash; + $self->{PARM_HASH} = \%parmhash; + $self->{PARM_CACHE} = {}; + } else { + $self->change_user($self->{USERNAME}, $self->{DOMAIN}); } + + return $self; +} + +# +# In some instances it is useful to be able to dynamically change the +# username/domain associated with a navmap (e.g. to navigate for someone +# else besides the current user...if sufficiently privileged. +# Parameters: +# user - New user. +# domain- Domain the user belongs to. +# Implicit inputs: +# +sub change_user { + my $self = shift; + $self->{USERNAME} = shift; + $self->{DOMAIN} = shift; + + # If the hashes are already tied make sure to break that bond: + + untie %{$self->{NAV_HASH}}; + untie %{$self->{PARM_HASH}}; + + # The assumption is that we have to + # use lonmap here to re-read the hash and from it reconstruct + # new big and parameter hashes. An implicit assumption at this time + # is that the course file is probably not created locally yet + # an that we will therefore just read without tying. + + my ($cdom, $cnum) = split(/\_/, $env{'request.course.id'}); + + my %big_hash; + &Apache::lonmap::loadmap($cnum, $cdom, $self->{USERNAME}, $self->{DOMAIN}, \%big_hash); + $self->{NAV_HASH} = \%big_hash; + + + + # Now clear the parm cache and reconstruct the parm hash fromt he big_hash + # param.xxxx keys. + + $self->{PARM_CACHE} = {}; - if (!(tie(%parmhash, 'GDBM_File', "${courseFn}_parms.db", - &GDBM_READER(), 0640))) - { - untie %{$self->{PARM_HASH}}; - return undef; + my %parm_hash = {}; + foreach my $key (keys %big_hash) { + if ($key =~ /^param\./) { + my $param_key = $key; + $param_key =~ s/^param\.//; + $parm_hash{$param_key} = $big_hash{$key}; + } } - $self->{NAV_HASH} = \%navmaphash; - $self->{PARM_HASH} = \%parmhash; - $self->{PARM_CACHE} = {}; + $self->{PARM_HASH} = \%parm_hash; - bless($self); - - return $self; -} + + +} sub generate_course_user_opt { my $self = shift; if ($self->{COURSE_USER_OPT_GENERATED}) { return; } - my $uname=$env{'user.name'}; - my $udom=$env{'user.domain'}; + my $uname=$self->{USERNAME}; + my $udom=$self->{DOMAIN}; + my $cid=$env{'request.course.id'}; my $cdom=$env{'course.'.$cid.'.domain'}; my $cnum=$env{'course.'.$cid.'.num'}; @@ -1885,6 +2147,8 @@ sub generate_course_user_opt { return; } + + sub generate_email_discuss_status { my $self = shift; my $symb = shift; @@ -1894,7 +2158,7 @@ sub generate_email_discuss_status { my $cdom=$env{'course.'.$cid.'.domain'}; my $cnum=$env{'course.'.$cid.'.num'}; - my %emailstatus = &Apache::lonnet::dump('email_status'); + my %emailstatus = &Apache::lonnet::dump('email_status',$self->{DOMAIN},$self->{USERNAME}); my $logoutTime = $emailstatus{'logout'}; my $courseLeaveTime = $emailstatus{'logout_'.$env{'request.course.id'}}; $self->{LAST_CHECK} = (($courseLeaveTime > $logoutTime) ? @@ -1902,7 +2166,7 @@ sub generate_email_discuss_status { my %discussiontime = &Apache::lonnet::dump('discussiontimes', $cdom, $cnum); my %lastread = &Apache::lonnet::dump('nohist_'.$cid.'_discuss', - $env{'user.domain'},$env{'user.name'},'lastread'); + $self->{DOMAIN},$self->{USERNAME},'lastread'); my %lastreadtime = (); foreach my $key (keys %lastread) { my $shortkey = $key; @@ -1912,8 +2176,8 @@ sub generate_email_discuss_status { my %feedback=(); my %error=(); - my @keys = &Apache::lonnet::getkeys('nohist_email',$env{'user.domain'}, - $env{'user.name'}); + my @keys = &Apache::lonnet::getkeys('nohist_email',$self->{DOMAIN}, + $self->{USERNAME}); foreach my $msgid (@keys) { if ((!$emailstatus{$msgid}) || ($emailstatus{$msgid} eq 'new')) { @@ -1961,8 +2225,8 @@ sub get_user_data { # Retrieve performance data on problems my %student_data = Apache::lonnet::currentdump($env{'request.course.id'}, - $env{'user.domain'}, - $env{'user.name'}); + $self->{DOMAIN}, + $self->{USERNAME}); $self->{STUDENT_DATA} = \%student_data; $self->{RETRIEVED_USER_DATA} = 1; @@ -2191,7 +2455,7 @@ resource object. 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): +=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. @@ -2276,20 +2540,34 @@ sub parmval { my $self = shift; my ($what,$symb,$recurse)=@_; my $hashkey = $what."|||".$symb; - + my $cache = $self->{PARM_CACHE}; if (defined($self->{PARM_CACHE}->{$hashkey})) { - return $self->{PARM_CACHE}->{$hashkey}; + if (ref($self->{PARM_CACHE}->{$hashkey}) eq 'ARRAY') { + if (defined($self->{PARM_CACHE}->{$hashkey}->[0])) { + if (wantarray) { + return @{$self->{PARM_CACHE}->{$hashkey}}; + } else { + return $self->{PARM_CACHE}->{$hashkey}->[0]; + } + } + } else { + return $self->{PARM_CACHE}->{$hashkey}; + } } - my $result = $self->parmval_real($what, $symb, $recurse); $self->{PARM_CACHE}->{$hashkey} = $result; - return $result; + if (wantarray) { + return @{$result}; + } + return $result->[0]; } + sub parmval_real { my $self = shift; my ($what,$symb,$recurse) = @_; + # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated $self->generate_course_user_opt(); @@ -2301,10 +2579,10 @@ sub parmval_real { @cgrps = sort(@cgrps); $cgroup = $cgrps[0]; } - my $uname=$env{'user.name'}; - my $udom=$env{'user.domain'}; + my $uname=$self->{USERNAME}; + my $udom=$self->{DOMAIN}; - unless ($symb) { return ''; } + unless ($symb) { return ['']; } my $result=''; my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); @@ -2318,66 +2596,72 @@ sub parmval_real { my $mapparm=$mapname.'___(all).'.$what; my $usercourseprefix=$cid; + + my $grplevel=$usercourseprefix.'.['.$cgroup.'].'.$what; my $grplevelr=$usercourseprefix.'.['.$cgroup.'].'.$symbparm; my $grplevelm=$usercourseprefix.'.['.$cgroup.'].'.$mapparm; + 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}; } + if (defined($$useropt{$courselevelr})) { return [$$useropt{$courselevelr},'resource']; } + if (defined($$useropt{$courselevelm})) { return [$$useropt{$courselevelm},'map']; } + if (defined($$useropt{$courselevel})) { return [$$useropt{$courselevel},'course']; } } # ------------------------------------------------------- second, check course if ($cgroup ne '' and defined($courseopt)) { - if (defined($$courseopt{$grplevelr})) { return $$courseopt{$grplevelr}; } - if (defined($$courseopt{$grplevelm})) { return $$courseopt{$grplevelm}; } - if (defined($$courseopt{$grplevel})) { return $$courseopt{$grplevel}; } + if (defined($$courseopt{$grplevelr})) { return [$$courseopt{$grplevelr},'resource']; } + if (defined($$courseopt{$grplevelm})) { return [$$courseopt{$grplevelm},'map']; } + if (defined($$courseopt{$grplevel})) { return [$$courseopt{$grplevel},'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{$seclevelr})) { return [$$courseopt{$seclevelr},'resource']; } + if (defined($$courseopt{$seclevelm})) { return [$$courseopt{$seclevelm},'map']; } + if (defined($$courseopt{$seclevel})) { return [$$courseopt{$seclevel},'course']; } } if (defined($courseopt)) { - if (defined($$courseopt{$courselevelr})) { return $$courseopt{$courselevelr}; } + if (defined($$courseopt{$courselevelr})) { return [$$courseopt{$courselevelr},'resource']; } } # ----------------------------------------------------- third, check map parms my $thisparm=$$parmhash{$symbparm}; - if (defined($thisparm)) { return $thisparm; } + if (defined($thisparm)) { return [$thisparm,'map']; } # ----------------------------------------------------- fourth , check default my $meta_rwhat=$rwhat; $meta_rwhat=~s/\./_/g; my $default=&Apache::lonnet::metadata($fn,$meta_rwhat); - if (defined($default)) { return $default} + if (defined($default)) { return [$default,'resource']} $default=&Apache::lonnet::metadata($fn,'parameter_'.$meta_rwhat); - if (defined($default)) { return $default} - + if (defined($default)) { return [$default,'resource']} # --------------------------------------------------- fifth, check more course if (defined($courseopt)) { - if (defined($$courseopt{$courselevelm})) { return $$courseopt{$courselevelm}; } - if (defined($$courseopt{$courselevel})) { return $$courseopt{$courselevel}; } + if (defined($$courseopt{$courselevelm})) { return [$$courseopt{$courselevelm},'map']; } + if (defined($$courseopt{$courselevel})) { + my $ret = [$$courseopt{$courselevel},'course']; + return $ret; + } } - # --------------------------------------------------- sixth , cascade up parts my ($space,@qualifier)=split(/\./,$rwhat); @@ -2387,14 +2671,225 @@ sub parmval_real { my $id=pop(@parts); my $part=join('_',@parts); if ($part eq '') { $part='0'; } - my $partgeneral=$self->parmval($part.".$qualifier",$symb,1); - if (defined($partgeneral)) { return $partgeneral; } + my @partgeneral=$self->parmval($part.".$qualifier",$symb,1); + if (defined($partgeneral[0])) { return \@partgeneral; } } - if ($recurse) { return undef; } + if ($recurse) { return []; } my $pack_def=&Apache::lonnet::packages_tab_default($fn,'resource.'.$rwhat); - if (defined($pack_def)) { return $pack_def; } - return ''; + if (defined($pack_def)) { return [$pack_def,'resource']; } + return ['']; } +# +# Determines the open/close dates for printing a map that +# encloses a resource. +# +sub map_printdates { + my ($self, $res, $part) = @_; + + + + + + my $opendate = $self->get_mapparam($res->symb(), "$part.printstartdate"); + my $closedate= $self->get_mapparam($res->symb(), "$part.printenddate"); + + + return ($opendate, $closedate); +} + +sub get_mapparam { + my ($self, $symb, $what) = @_; + + # Ensure the course option hash is populated: + + $self->generate_course_user_opt(); + + # Get the course id and section if there is one. + + my $cid=$env{'request.course.id'}; + my $csec=$env{'request.course.sec'}; + my $cgroup=''; + my @cgrps=split(/:/,$env{'request.course.groups'}); + if (@cgrps > 0) { + @cgrps = sort(@cgrps); + $cgroup = $cgrps[0]; + } + my $uname=$self->{USERNAME}; + my $udom=$self->{DOMAIN}; + + unless ($symb) { return ['']; } + my $result=''; + + + # Figure out which map we are in. + + my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); + $mapname = &Apache::lonnet::deversion($mapname); + + + my $rwhat=$what; + $what=~s/^parameter\_//; + $what=~s/\_/\./; + + # Build the hash keys for the lookup: + + my $symbparm=$symb.'.'.$what; + my $mapparm=$mapname.'___(all).'.$what; + my $usercourseprefix=$cid; + + + my $grplevel = "$usercourseprefix.[$cgroup].$mapparm"; + my $seclevel = "$usercourseprefix.[$csec].$mapparm"; + my $courselevel = "$usercourseprefix.$mapparm"; + + + # Get handy references to the hashes we need in $self: + + my $useropt = $self->{USER_OPT}; + my $courseopt = $self->{COURSE_OPT}; + my $parmhash = $self->{PARM_HASH}; + + # Check per user + + + + if ($uname and defined($useropt)) { + if (defined($$useropt{$courselevel})) { + return $$useropt{$courselevel}; + } + } + + # Check course -- group + + + + if ($cgroup ne '' and defined ($courseopt)) { + if (defined($$courseopt{$grplevel})) { + return $$courseopt{$grplevel}; + } + } + + # Check course -- section + + + + + + if ($csec and defined($courseopt)) { + if (defined($$courseopt{$seclevel})) { + return $$courseopt{$seclevel}; + } + } + # Check the map parameters themselves: + + my $thisparm = $$parmhash{$symbparm}; + if (defined($thisparm)) { + return $thisparm; + } + + + # Additional course parameters: + + if (defined($courseopt)) { + if (defined($$courseopt{$courselevel})) { + return $$courseopt{$courselevel}; + } + } + return undef; # Unefined if we got here. +} + +sub course_printdates { + my ($self, $symb, $part) = @_; + + + my $opendate = $self->getcourseparam($symb, $part . '.printstartdate'); + my $closedate = $self->getcourseparam($symb, $part . '.printenddate'); + return ($opendate, $closedate); + +} + +sub getcourseparam { + my ($self, $symb, $what) = @_; + + $self->generate_course_user_opt(); # If necessary populate the hashes. + + my $uname = $self->{USERNAME}; + my $udom = $self->{DOMAIN}; + + # Course, section, group ids come from the env: + + my $cid = $env{'request.course.id'}; + my $csec = $env{'request.course.sec'}; + my $cgroup = ''; # Assume no group + + my @cgroups = split(/:/, $env{'request.course.groups'}); + if(@cgroups > 0) { + @cgroups = sort(@cgroups); + $cgroup = $cgroups[0]; # There is a course group. + } + my ($mapname,$id,$fn)=&Apache::lonnet::decode_symb($symb); + $mapname = &Apache::lonnet::deversion($mapname); + + # + # Make the various lookup keys: + # + + $what=~s/^parameter\_//; + $what=~s/\_/\./; + + + my $symbparm = $symb . '.' . $what; + my $mapparm=$mapname.'___(all).'.$what; + + # Local refs to the hashes we're going to look at: + + my $useropt = $self->{USER_OPT}; + my $courseopt = $self->{COURSE_OPT}; + + # + # We want the course level stuff from the way + # parmval_real operates + # TODO: Fator some of this stuff out of + # both parmval_real and here + # + my $courselevel = $cid . '.' . $what; + my $grplevel = $cid . '.[' . $cgroup . ']' . $what; + my $seclevel = $cid . '.[' . $csec . ']' . $what; + + + # Try for the user's course level option: + + if ($uname and defined($useropt)) { + if (defined($$useropt{$courselevel})) { + return $$useropt{$courselevel}; + } + } + # Try for the group's course level option: + + if ($uname ne '' and defined($courseopt)) { + if (defined($$courseopt{$grplevel})) { + return $$courseopt{$grplevel}; + } + } + + # Try for section level parameters: + + if ($csec and defined($courseopt)) { + if (defined($$courseopt{$seclevel})) { + return $$courseopt{$seclevel}; + } + } + # Try for 'additional' course parameterse: + + if (defined($courseopt)) { + if (defined($$courseopt{$courselevel})) { + return $$courseopt{$courselevel}; + } + } + return undef; + +} + =pod @@ -2448,7 +2943,7 @@ in the filter function. Retrieves version infomation for a url. Returns the version (a number, or the string "mostrecent") for resources which have version information in the big hash. - + =cut @@ -2703,6 +3198,9 @@ sub new { weaken($self->{NAV_MAP} = shift); return undef unless ($self->{NAV_MAP}); + $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME}; + $self->{DOMAIN} = $self->{NAV_MAP}->{DOMAIN}; + # Handle the parameters $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); @@ -2816,12 +3314,14 @@ sub new { if ($resourceCount == 1 && $resource->is_sequence() && !$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}, - $self->{FORCE_TOP}); + my $result; + $result = Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, + $finishResource, $self->{FILTER}, + $self->{ALREADY_SEEN}, + $self->{CONDITION}, + $self->{FORCE_TOP}); + return $result; + } @@ -2841,7 +3341,6 @@ sub new { $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; bless ($self); - return $self; } @@ -2856,8 +3355,13 @@ sub next { # do so. if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0}) { $self->{HAVE_RETURNED_0} = 1; + my $nextTopLevel = $self->{NAV_MAP}->getById('0.0'); return $self->{NAV_MAP}->getById('0.0'); } + if ($self->{RETURN_0} && !$self->{HAVE_RETURNED_0_BEGIN_MAP}) { + $self->{HAVE_RETURNED_0_BEGIN_MAP} = 1; + return $self->BEGIN_MAP(); + } if ($self->{RECURSIVE_ITERATOR_FLAG}) { # grab the next from the recursive iterator @@ -2871,7 +3375,6 @@ sub next { if ($self->{RECURSIVE_DEPTH} == 0) { $self->{RECURSIVE_ITERATOR_FLAG} = 0; } - return $next; } @@ -2947,6 +3450,8 @@ sub next { # 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) { @@ -2980,7 +3485,6 @@ sub next { $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}, @@ -3072,6 +3576,9 @@ sub new { weaken($self->{NAV_MAP} = shift); return undef unless ($self->{NAV_MAP}); + $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME}; + $self->{DOMAIN} = $self->{NAV_MAP}->{DOMAIN}; + $self->{FIRST_RESOURCE} = shift || $self->{NAV_MAP}->firstResource(); $self->{FINISH_RESOURCE} = shift || $self->{NAV_MAP}->finishResource(); @@ -3308,10 +3815,15 @@ sub new { weaken($self->{NAV_MAP} = shift); $self->{ID} = shift; + $self->{USERNAME} = $self->{NAV_MAP}->{USERNAME}; + $self->{DOMAIN} = $self->{NAV_MAP}->{DOMAIN}; + # Store this new resource in the parent nav map's cache. $self->{NAV_MAP}->{RESOURCE_CACHE}->{$self->{ID}} = $self; $self->{RESOURCE_ERROR} = 0; + $self->{DUEDATE_CACHE} = undef; + # A hash that can be used by two-pass algorithms to store data # about this resource in. Not used by the resource object # directly. @@ -3329,7 +3841,11 @@ sub navHash { my $self = shift; my $param = shift; my $id = shift; - return $self->{NAV_MAP}->navhash($param . ($id?$self->{ID}:"")); + my $arg = $param . ($id?$self->{ID}:""); + if (ref($self) && ref($self->{NAV_MAP}) && defined($arg)) { + return $self->{NAV_MAP}->navhash($arg); + } + return; } =pod @@ -3407,11 +3923,13 @@ sub kind { my $self=shift; return $self- sub randomout { my $self=shift; return $self->navHash("randomout_", 1); } sub randompick { my $self = shift; - return $self->parmval('randompick'); + my $randompick = $self->parmval('randompick'); + return $randompick; } sub randomorder { my $self = shift; - return ($self->parmval('randomorder') =~ /^yes$/i); + my $randomorder = $self->parmval('randomorder'); + return ($randomorder =~ /^yes$/i); } sub link { my $self=shift; @@ -3532,7 +4050,8 @@ sub retrieveResources { sub is_exam { my ($self,$part) = @_; - if ($self->parmval('type',$part) eq 'exam') { + my $type = $self->parmval('type',$part); + if ($type eq 'exam') { return 1; } if ($self->src() =~ /\.(exam)$/) { @@ -3555,7 +4074,8 @@ sub is_page { sub is_practice { my $self=shift; my ($part) = @_; - if ($self->parmval('type',$part) eq 'practice') { + my $type = $self->parmval('type',$part); + if ($type eq 'practice') { return 1; } return 0; @@ -3563,11 +4083,47 @@ sub is_practice { sub is_problem { my $self=shift; my $src = $self->src(); - if ($src =~ /\.(problem|exam|quiz|assess|survey|form|library|task)$/) { + if ($src =~ /$LONCAPA::assess_re/) { return !($self->is_practice()); } return 0; } +# +# The has below is the set of status that are considered 'incomplete' +# +my %incomplete_hash = +( + TRIES_LEFT() => 1, + OPEN() => 1, + ATTEMPTED() => 1 + + ); +# +# Return tru if a problem is incomplete... for now incomplete means that +# any part of the problem is incomplete. +# Note that if the resources is not a problem, 0 is returned. +# +sub is_incomplete { + my $self = shift; + if ($self->is_problem()) { + foreach my $part (@{$self->parts()}) { + if (exists($incomplete_hash{$self->status($part)})) { + return 1; + } + } + } + return 0; + +} +sub is_raw_problem { + my $self=shift; + my $src = $self->src(); + if ($src =~ /$LONCAPA::assess_re/) { + return 1; + } + return 0; +} + sub contains_problem { my $self=shift; if ($self->is_page()) { @@ -3576,15 +4132,25 @@ sub contains_problem { } return 0; } +sub map_contains_problem { + my $self=shift; + if ($self->is_map()) { + my $has_problem= + $self->hasResource($self,sub { $_[0]->is_problem() },1); + return $has_problem; + } + return 0; +} sub is_sequence { my $self=shift; return $self->navHash("is_map_", 1) && - $self->navHash("map_type_" . $self->map_pc()) eq 'sequence'; + $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') { + my $type = $self->parmval('type',$part); + if (($type eq 'survey') || ($type eq 'surveycred')) { return 1; } if ($self->src() =~ /\.(survey)$/) { @@ -3592,6 +4158,15 @@ sub is_survey { } return 0; } +sub is_anonsurvey { + my $self = shift(); + my $part = shift(); + my $type = $self->parmval('type',$part); + if (($type eq 'anonsurvey') || ($type eq 'anonsurveycred')) { + return 1; + } + return 0; +} sub is_task { my $self=shift; my $src = $self->src(); @@ -3645,6 +4220,12 @@ resource of the map. Returns a string with the type of the map in it. +=item *B: + +Returns a string with a comma-separated ordered list of map_pc IDs +for the hierarchy of maps containing a map, with the top level +map first, then descending to deeper levels, with the enclosing map last. + =back =cut @@ -3675,6 +4256,11 @@ sub map_type { my $pc = $self->map_pc(); return $self->navHash("map_type_$pc", 0); } +sub map_hierarchy { + my $self = shift; + my $pc = $self->map_pc(); + return $self->navHash("map_hierarchy_$pc", 0); +} ##### # Property queries @@ -3705,7 +4291,19 @@ their code.) =over 4 -=item * B: + +=item * B + +returns true if the current date is such that the +specified resource part is printable. + + +=item * B + +Returns true if all parts in the resource are printable making the +entire resource printable. + +=item * B Get the Client IP/Name Access Control information. @@ -3758,18 +4356,88 @@ Get the weight for the problem. =cut + + + +sub printable { + + my ($self, $part) = @_; + + &Apache::lonnet::logthis($self->symb()); + + + # The following cases apply: + # - If a start date is not set, it is replaced by the open date. + # - Ditto for start/open replaced by content open. + # - If neither start nor printdates are set the part is printable. + # - Start date set but no end date: Printable if now >= start date. + # - End date set but no start date: Printable if now <= end date. + # - both defined: printable if start <= now <= end + # + + # Get the print open/close dates for the resource. + + my $start = $self->parmval("printstartdate", $part); + my $end = $self->parmval("printenddate", $part); + + if (!$start) { + $start = $self->parmval("opendate", $part); + } + if (!$start) { + $start = $self->parmval("contentopen", $part); + } + + + my $now = time(); + + + my $startok = 1; + my $endok = 1; + + if ((defined $start) && ($start ne '')) { + $startok = $start <= $now; + } + if ((defined $end) && ($end != '')) { + $endok = $end >= $now; + } + return $startok && $endok; +} + +sub resprintable { + my $self = shift; + + # get parts...or realize there are no parts. + + my $partsref = $self->parts(); + my @parts = @$partsref; + + if ((!defined(@parts)) || (scalar(@parts) == 0)) { + return $self->printable(0); + } else { + foreach my $part (@parts) { + if (!$self->printable($part)) { + return 0; + } + } + return 1; + } +} + sub acc { (my $self, my $part) = @_; - return $self->parmval("acc", $part); + my $acc = $self->parmval("acc", $part); + return $acc; } 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); + my $answerdatetype = $self->parmval("answerdate.type", $part); + my $answerdate = $self->parmval("answerdate", $part); + my $duedate = $self->parmval("duedate", $part); + if ($answerdatetype eq 'date_interval') { + $answerdate = $duedate + $answerdate; } - return $self->parmval("answerdate", $part); + return $answerdate; } sub awarded { my $self = shift; my $part = shift; @@ -3777,23 +4445,63 @@ sub awarded { if (!defined($part)) { $part = '0'; } return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.awarded'}; } +sub taskversion { + my $self = shift; my $part = shift; + $self->{NAV_MAP}->get_user_data(); + if (!defined($part)) { $part = '0'; } + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.version'}; +} +sub taskstatus { + my $self = shift; my $part = shift; + $self->{NAV_MAP}->get_user_data(); + if (!defined($part)) { $part = '0'; } + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$self->taskversion($part).'.'.$part.'.status'}; +} +sub solved { + my $self = shift; my $part = shift; + $self->{NAV_MAP}->get_user_data(); + if (!defined($part)) { $part = '0'; } + return $self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.solved'}; +} +sub checkedin { + my $self = shift; my $part = shift; + $self->{NAV_MAP}->get_user_data(); + if (!defined($part)) { $part = '0'; } + if ($self->is_task()) { + my $version = $self->taskversion($part); + return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$version .'.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$version .'.'.$part.'.checkedin.slot'}); + } else { + return ($self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.checkedin'},$self->{NAV_MAP}->{STUDENT_DATA}->{$self->symb()}->{'resource.'.$part.'.checkedin.slot'}); + } +} # this should work exactly like the copy in lonhomework.pm +# Why is there a copy in lonhomework? Why not centralized? +# +# TODO: Centralize duedate. +# + sub duedate { (my $self, my $part) = @_; + if (defined ($self->{DUEDATE_CACHE}->{$part})) { + return $self->{DUEDATE_CACHE}->{$part}; + } my $date; - my $interval=$self->parmval("interval", $part); + my @interval=$self->parmval("interval", $part); my $due_date=$self->parmval("duedate", $part); - if ($interval =~ /\d+/) { - my $first_access=&Apache::lonnet::get_first_access('map',$self->symb); + if ($interval[0] =~ /\d+/) { + my $first_access=&Apache::lonnet::get_first_access($interval[1], + $self->symb); if (defined($first_access)) { - $interval = $first_access+$interval; - $date = ($interval < $due_date)? $interval : $due_date; + my $interval = $first_access+$interval[0]; + $date = (!$due_date || $interval < $due_date) ? $interval + : $due_date; } else { $date = $due_date; } } else { $date = $due_date; } + $self->{DUEDATE_CACHE}->{$part} = $date; return $date; } sub handgrade { @@ -3801,39 +4509,46 @@ sub handgrade { my @response_ids = $self->responseIds($part); if (@response_ids) { foreach my $response_id (@response_ids) { - if (lc($self->parmval("handgrade",$part.'_'.$response_id)) - eq 'yes') { + my $handgrade = $self->parmval("handgrade",$part.'_'.$response_id); + if (lc($handgrade) eq 'yes') { return 'yes'; } } } - return $self->parmval("handgrade", $part); + my $handgrade = $self->parmval("handgrade", $part); + return $handgrade; } sub maxtries { (my $self, my $part) = @_; - return $self->parmval("maxtries", $part); + my $maxtries = $self->parmval("maxtries", $part); + return $maxtries; } sub opendate { (my $self, my $part) = @_; - if ($self->parmval("opendate.type", $part) eq 'date_interval') { - return $self->duedate($part) - - $self->parmval("opendate", $part); + my $opendatetype = $self->parmval("opendate.type", $part); + my $opendate = $self->parmval("opendate", $part); + if ($opendatetype eq 'date_interval') { + my $duedate = $self->duedate($part); + $opendate = $duedate - $opendate; } - return $self->parmval("opendate"); + return $opendate; } sub problemstatus { (my $self, my $part) = @_; - return lc $self->parmval("problemstatus", $part); + my $problemstatus = $self->parmval("problemstatus", $part); + return lc($problemstatus); } sub sig { (my $self, my $part) = @_; - return $self->parmval("sig", $part); + my $sig = $self->parmval("sig", $part); + return $sig; } sub tol { (my $self, my $part) = @_; - return $self->parmval("tol", $part); + my $tol = $self->parmval("tol", $part); + return $tol; } -sub tries { +sub tries { my $self = shift; my $tries = $self->queryRestoreHash('tries', shift); if (!defined($tries)) { return '0';} @@ -3841,15 +4556,17 @@ sub tries { } sub type { (my $self, my $part) = @_; - return $self->parmval("type", $part); + my $type = $self->parmval("type", $part); + return $type; } sub weight { my $self = shift; my $part = shift; if (!defined($part)) { $part = '0'; } - return &Apache::lonnet::EXT('resource.'.$part.'.weight', - $self->symb(), $env{'user.domain'}, - $env{'user.name'}, - $env{'request.course.sec'}); + my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $self->symb(), $self->{DOMAIN}, + $self->{USERNAME}, + $env{'request.course.sec'}); + return $weight; } sub part_display { my $self= shift(); my $partID = shift(); @@ -3861,13 +4578,21 @@ sub part_display { } return $display; } +sub slot_control { + my $self=shift(); my $part = shift(); + if (!defined($part)) { $part = '0'; } + my $useslots = $self->parmval("useslots", $part); + my $availablestudent = $self->parmval("availablestudent", $part); + my $available = $self->parmval("available", $part); + return ($useslots,$availablestudent,$available); +} # Multiple things need this sub getReturnHash { my $self = shift; if (!defined($self->{RETURN_HASH})) { - my %tmpHash = &Apache::lonnet::restore($self->symb()); + my %tmpHash = &Apache::lonnet::restore($self->symb(),undef,$self->{DOMAIN},$self->{USERNAME}); $self->{RETURN_HASH} = \%tmpHash; } } @@ -4293,7 +5018,7 @@ 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 NOTHING_SET { return 5; } sub NETWORK_FAILURE { return 100; } # getDateStatus gets the date status for a given problem part. @@ -4379,6 +5104,10 @@ Information not available due to network Attempted, and not yet graded. +=item * B: + +Attempted, and credit received for attempt (survey and anonymous survey only). + =back =cut @@ -4390,6 +5119,7 @@ sub CORRECT { return 13; } sub CORRECT_BY_OVERRIDE { return 14; } sub EXCUSED { return 15; } sub ATTEMPTED { return 16; } +sub CREDIT_ATTEMPTED { return 17; } sub getCompletionStatus { my $self = shift; @@ -4408,6 +5138,13 @@ sub getCompletionStatus { 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; } + if ($status eq 'credit_attempted') { + if ($self->is_anonsurvey($part) || $self->is_survey($part)) { + return $self->CREDIT_ATTEMPTED; + } else { + return $self->ATTEMPTED; + } + } return $self->NOT_ATTEMPTED; } @@ -4497,6 +5234,10 @@ The item is open and not yet tried. The problem has been attempted. +=item * B: + +The problem has been attempted, and credit given for the attempt (survey and anonymous survey only). + =item * B: An answer has been submitted, but the student should not see it. @@ -4505,9 +5246,20 @@ An answer has been submitted, but the st =cut -sub TRIES_LEFT { return 20; } -sub ANSWER_SUBMITTED { return 21; } -sub PARTIALLY_CORRECT{ return 22; } +sub TRIES_LEFT { return 20; } +sub ANSWER_SUBMITTED { return 21; } +sub PARTIALLY_CORRECT { return 22; } + +sub RESERVED_LATER { return 30; } +sub RESERVED { return 31; } +sub RESERVED_LOCATION { return 32; } +sub RESERVABLE { return 33; } +sub RESERVABLE_LATER { return 34; } +sub NOTRESERVABLE { return 35; } +sub NOT_IN_A_SLOT { return 36; } +sub NEEDS_CHECKIN { return 37; } +sub WAITING_FOR_GRADE { return 38; } +sub UNKNOWN { return 39; } sub status { my $self = shift; @@ -4523,7 +5275,11 @@ sub status { #if ($self->{RESOURCE_ERROR}) { return NETWORK_FAILURE; } if ($completionStatus == NETWORK_FAILURE) { return NETWORK_FAILURE; } - my $suppressFeedback = $self->problemstatus($part) eq 'no'; + my $suppressFeedback = 0; + if (($self->problemstatus($part) eq 'no') || + ($self->problemstatus($part) eq 'no_feedback_ever')) { + $suppressFeedback = 1; + } # If there's an answer date and we're past it, don't # suppress the feedback; student should know if ($self->duedate($part) && $self->duedate($part) < time() && @@ -4555,6 +5311,10 @@ sub status { return ATTEMPTED; } + if ($completionStatus == CREDIT_ATTEMPTED) { + return CREDIT_ATTEMPTED; + } + # If it's EXCUSED, then return that no matter what if ($completionStatus == EXCUSED) { return EXCUSED; @@ -4593,7 +5353,106 @@ sub status { } # Otherwise, it's untried and open - return OPEN; + return OPEN; +} + +sub check_for_slot { + my $self = shift; + my $part = shift; + my $symb = $self->symb(); + my ($use_slots,$available,$availablestudent) = $self->slot_control($part); + if (($use_slots ne '') && ($use_slots !~ /^\s*no\s*$/i)) { + my @slots = (split(/:/,$availablestudent),split(/:/,$available)); + my $cid=$env{'request.course.id'}; + my $cdom=$env{'course.'.$cid.'.domain'}; + my $cnum=$env{'course.'.$cid.'.num'}; + my $now = time; + my $num_usable_slots = 0; + if (@slots > 0) { + my %slots=&Apache::lonnet::get('slots',[@slots],$cdom,$cnum); + if (&Apache::lonnet::error(%slots)) { + return (UNKNOWN); + } + my @sorted_slots = &Apache::loncommon::sorted_slots(\@slots,\%slots,'starttime'); + my ($checkedin,$checkedinslot); + foreach my $slot_name (@sorted_slots) { + next if (!defined($slots{$slot_name}) || !ref($slots{$slot_name})); + my $end = $slots{$slot_name}->{'endtime'}; + my $start = $slots{$slot_name}->{'starttime'}; + my $ip = $slots{$slot_name}->{'ip'}; + if ($self->simpleStatus() == OPEN) { + if ($end > $now) { + if ($start > $now) { + return (RESERVED_LATER,$start,$slot_name); + } else { + if ($ip ne '') { + if (!&Apache::loncommon::check_ip_acc($ip)) { + return (RESERVED_LOCATION,$ip,$slot_name); + } + } + my @proctors; + if ($slots{$slot_name}->{'proctor'} ne '') { + @proctors = split(',',$slots{$slot_name}->{'proctor'}); + } + if (@proctors > 0) { + ($checkedin,$checkedinslot) = $self->checkedin(); + unless ((grep(/^\Q$checkedin\E/,@proctors)) && + ($checkedinslot eq $slot_name)) { + return (NEEDS_CHECKIN,undef,$slot_name); + } + } + return (RESERVED,$end,$slot_name); + } + } + } elsif ($end > $now) { + $num_usable_slots ++; + } + } + my ($is_correct,$got_grade); + if ($self->is_task()) { + my $taskstatus = $self->taskstatus(); + $is_correct = (($taskstatus eq 'pass') || + ($self->solved() =~ /^correct_/)); + $got_grade = ($taskstatus =~ /^(?:pass|fail)$/); + } else { + $got_grade = 1; + $is_correct = ($self->solved() =~ /^correct_/); + } + ($checkedin,$checkedinslot) = $self->checkedin(); + if ($checkedin) { + if (!$got_grade) { + return (WAITING_FOR_GRADE); + } elsif ($is_correct) { + return (CORRECT); + } + } + if ($num_usable_slots) { + return(NOT_IN_A_SLOT); + } + } + my $reservable = &Apache::lonnet::get_reservable_slots($cnum,$cdom,$env{'user.name'}, + $env{'user.domain'}); + if (ref($reservable) eq 'HASH') { + if ((ref($reservable->{'now_order'}) eq 'ARRAY') && (ref($reservable->{'now'}) eq 'HASH')) { + foreach my $slot (reverse (@{$reservable->{'now_order'}})) { + if (($reservable->{'now'}{$slot}{'symb'} eq '') || + ($reservable->{'now'}{$slot}{'symb'} eq $symb)) { + return(RESERVABLE,$reservable->{'now'}{$slot}{'endreserve'}); + } + } + } + if ((ref($reservable->{'future_order'}) eq 'ARRAY') && (ref($reservable->{'future'}) eq 'HASH')) { + foreach my $slot (@{$reservable->{'future_order'}}) { + if (($reservable->{'future'}{$slot}{'symb'} eq '') || + ($reservable->{'future'}{$slot}{'symb'} eq $symb)) { + return(RESERVABLE_LATER,$reservable->{'future'}{$slot}{'startreserve'}); + } + } + } + } + return(NOTRESERVABLE); + } + return; } sub CLOSED { return 23; } @@ -4645,6 +5504,7 @@ my %compositeToSimple = INCORRECT() => INCORRECT, OPEN() => OPEN, ATTEMPTED() => ATTEMPTED, + CREDIT_ATTEMPTED() => CORRECT, ANSWER_SUBMITTED() => ATTEMPTED ); @@ -4719,6 +5579,7 @@ sub completable { # and it is not "attempted" (manually graded problem), it is # not "complete" if ($self->getCompletionStatus($part) == ATTEMPTED() || + $self->getCompletionStatus($part) == CREDIT_ATTEMPTED() || $status == ANSWER_SUBMITTED() ) { # did this part already, as well as we can next;