--- loncom/interface/lonnavmaps.pm 2009/02/24 22:05:26 1.427 +++ loncom/interface/lonnavmaps.pm 2014/12/11 01:56:34 1.504 @@ -1,7 +1,8 @@ # The LearningOnline Network with CAPA # Navigate Maps Handler # -# $Id: lonnavmaps.pm,v 1.427 2009/02/24 22:05:26 droeschl Exp $ +# $Id: lonnavmaps.pm,v 1.504 2014/12/11 01:56:34 raeburn Exp $ + # # Copyright Michigan State University Board of Trustees # @@ -31,7 +32,7 @@ =head1 NAME -Apache::lonnavmaps.pm +Apache::lonnavmaps - Subroutines to handle and render the navigation =head1 SYNOPSIS @@ -163,6 +164,12 @@ If true, the resource's folder will not it. Default is false. True implies printCloseAll is false, since you can't close or open folders when this is on anyhow. +=item * B: + +If true, the title of the folder or page will not be followed by an +icon/link to direct editing of a folder or composite page, originally +added via the Course Editor. + =back =item * B: @@ -463,7 +470,7 @@ returns 4 =item add_linkitem() -=item show_linkitems() +=item show_linkitems_toolbar() =back @@ -477,10 +484,18 @@ 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; } @@ -491,7 +506,7 @@ sub NOTHING { return 3; } my $resObj = "Apache::lonnavmaps::resource"; -# Keep these mappings in sync with lonquickgrades, which uses the colors +# Keep these mappings in sync with lonquickgrades, which usesthe colors # instead of the icons. my %statusIconMap = ( @@ -504,10 +519,13 @@ my %statusIconMap = $resObj->ERROR => '' ); -my %iconAltTags = - ( 'navmap.correct.gif' => 'Correct', - 'navmap.wrong.gif' => 'Incorrect', - 'navmap.open.gif' => 'Open' ); +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 = @@ -523,6 +541,7 @@ my %colormap = $resObj->OPEN => '', $resObj->NOTHING_SET => '', $resObj->ATTEMPTED => '', + $resObj->CREDIT_ATTEMPTED => '', $resObj->ANSWER_SUBMITTED => '', $resObj->PARTIALLY_CORRECT => '#006600' ); @@ -530,35 +549,6 @@ my %colormap = # 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 -} - - sub addToFilter { my $hashIn = shift; my $addition = shift; @@ -594,7 +584,7 @@ sub getLinkForResource { my ($map,$id,$src)=&Apache::lonnet::decode_symb($res->symb()); if ($map=~/\.page$/) { my $url=&Apache::lonnet::clutter($map); - $anchor=&escape($src->shown_symb()); + $anchor=&escape($res->shown_symb()); return ($url,$res->shown_symb(),$anchor); } } @@ -629,57 +619,95 @@ sub getDescription { return &mt("Having technical difficulties; please check status later"); } if ($status == $res->NOTHING_SET) { - return &mt("Not currently assigned."); + return &Apache::lonhtmlcommon::direct_parm_link(&mt('Not currently assigned'),$res->symb(),'opendate',$part); } if ($status == $res->OPEN_LATER) { - return &mt("Open ") .timeToHumanString($open,'start'); + 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 ")." " .timeToHumanString($due,'start'); + return &mt("Closes [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'duedate',$part)); } else { - return &mt("Due")." " .timeToHumanString($due,'end'); + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)); } } else { - return &mt("Open, no due date"); + 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")." " .timeToHumanString($answer,'start'); + 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")." " . timeToHumanString($due,'start'); + return &mt("Closed [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'start'),$res->symb(),'answerdate,duedate',$part)); } else { - return &mt("Was due")." " . timeToHumanString($due,'end'); + 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 &mt("Answer available"); + 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) { - return &mt("Answer submitted, not yet graded"); + 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).')'; + $triesString = '('.&mt('[_1] of [quant,_2,try,tries] used',$tries,$maxtries).')'; if ($maxtries > 1 && $maxtries - $tries == 1) { $triesString = "$triesString"; } } if ($due) { - return &mt("Due")." " . timeToHumanString($due,'end') . + return &mt("Due [_1]",&Apache::lonhtmlcommon::direct_parm_link(&timeToHumanString($due,'end'),$res->symb(),'duedate',$part)) . " $triesString"; } else { - return &mt("No due date")." $triesString"; + return &Apache::lonhtmlcommon::direct_parm_link(&mt("No due date"),$res->symb(),'duedate',$part)." $triesString"; } } if ($status == $res->ANSWER_SUBMITTED) { @@ -761,14 +789,31 @@ sub timeToHumanString { # Less than an hour if ( $delta < $hour ) { - # If so, use minutes + # 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 + # display hours + minutes, (and + seconds, if format specified it) if ( $delta < $hour * 24) { my $hours = floor($delta / $hour); my $minutes = floor(($delta % $hour) / $minute); @@ -783,15 +828,30 @@ sub timeToHumanString { 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. + # If there's a caller supplied format, use it, unless it only displays + # H:M:S or H:M. - if ($format ne '') { + if (($format ne '') && ($format ne '%T') && ($format ne '%R')) { my $timeStr = $dt->strftime($format); return $timeStr.' ('.$dt->time_zone_short_name().')'; } @@ -843,16 +903,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"}; @@ -865,7 +921,7 @@ 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 @@ -929,16 +985,26 @@ sub render_resource { $linkopen = ""; $linkclose = ""; } + if ((&Apache::lonnet::allowed('mdc',$env{'request.course.id'})) && + ($resource->symb=~/\_\_\_[^\_]+\_\_\_uploaded/)) { + if (!$params->{'map_no_edit_link'}) { + 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').''; + $nonLinkedText .=' '.&mt('not graded').''; } # We're done preparing and finally ready to start the rendering @@ -962,8 +1028,10 @@ sub render_resource { # Is this the current resource? if (!$params->{'displayedHereMarker'} && $resource->symb() eq $params->{'here'} ) { - $curMarkerBegin = ''; - $curMarkerEnd = ''; + unless ($resource->is_map()) { + $curMarkerBegin = ''; + $curMarkerEnd = ''; + } $params->{'displayedHereMarker'} = 1; } @@ -979,14 +1047,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$editmapLink$nonLinkedText"; } else { - $result .= "$curMarkerBegin$linkopen$title$partLabel$curMarkerEnd$nonLinkedText"; + $result .= "$curMarkerBegin$linkopen$title$partLabel$curMarkerEnd$editmapLink$nonLinkedText"; } return $result; @@ -997,16 +1061,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; } @@ -1014,9 +1075,9 @@ sub render_communication_status { my $feedback = $resource->getFeedback(); foreach my $msgid (split(/\,/, $feedback)) { if ($msgid) { - $feedbackHTML .= ' ' - . ''.&mt('New E-mail').''; + . ''.&mt('New E-mail').''; } } } @@ -1028,9 +1089,9 @@ sub render_communication_status { last if ($errorcount>=10); # Only output 10 bombs maximum if ($msgid) { $errorcount++; - $errorHTML .= ' ' - . ''.&mt('New Error').''; + . ''.&mt('New Error').''; } } } @@ -1048,11 +1109,7 @@ 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 .= ''; @@ -1063,7 +1120,7 @@ sub render_quick_status { if ($icon) { my $location= &Apache::loncommon::lonhttpdurl("/adm/lonIcons/$icon"); - $result .= "$linkopen$alt$linkclose"; + $result .= $linkopen.''.&mt($alt).''.$linkclose; } else { $result .= " "; } @@ -1080,21 +1137,28 @@ sub render_long_status { $params->{'multipart'} && $part eq "0"; my $color; + my $info = ''; if ($resource->is_problem() || $resource->is_practice()) { $color = $colormap{$resource->status}; - - if (dueInLessThan24Hours($resource, $part) || - lastTry($resource, $part)) { + + if (dueInLessThan24Hours($resource, $part)) { $color = $hurryUpColor; - } + $info = ' title="'.&mt('Due in less than 24 hours!').'"'; + } elsif (lastTry($resource, $part)) { + unless (($resource->problemstatus($part) eq 'no') || + ($resource->problemstatus($part) eq 'no_feedback_ever')) { + $color = $hurryUpColor; + $info = ' title="'.&mt('One try remaining!').'"'; + } + } } if ($resource->kind() eq "res" && - ($resource->is_problem() || $resource->is_practice()) && + $resource->is_raw_problem() && !$firstDisplayed) { - if ($color) {$result .= ""; } + if ($color) {$result .= ''; } $result .= getDescription($resource, $part); - if ($color) {$result .= ""; } + if ($color) {$result .= ""; } } if ($resource->is_map() && &advancedUser() && $resource->randompick()) { $result .= &mt('(randomly select [_1])', $resource->randompick()); @@ -1176,11 +1240,11 @@ sub render_parts_summary_status { } $return.= $td . $totalParts . ' parts: '; foreach my $status (@statuses) { - if ($overallstatus{$status}) { - $return.="" . $overallstatus{$status} . ' ' - . $statusStrings{$status} . ""; - } + if ($overallstatus{$status}) { + $return.='' . $overallstatus{$status} . ' ' + . $statusStrings{$status} . ''; + } } $return.= $endtd; return $return; @@ -1387,15 +1451,15 @@ sub render { 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})). '  '. - ' '.&mt('New message (click to open)').'

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

'. ''; } else { $result .= '  '. - ' '.&mt('Discussions').''. - '   '.&mt('New message (click to open)'). + ' '.&mt('Discussions').''. + '   '.&mt('New message (click to open)'). ''; } @@ -1405,34 +1469,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 = ''; @@ -1453,27 +1526,20 @@ 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 .= ''; - 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'}; } @@ -1486,18 +1552,16 @@ END } # End parameter setting - $result .= "\n\n\n\n\n\n\n
 
\n"; $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 = ("LC_trEven", "LC_trOdd"); # Shared variables $args->{'counter'} = 0; # counts the rows @@ -1505,7 +1569,7 @@ END $args->{'isNewBranch'} = 0; $args->{'condensed'} = 0; - my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace_21_21.gif"); + my $location = &Apache::loncommon::lonhttpdurl("/adm/lonIcons/whitespace_21.gif"); $args->{'indentString'} = setDefault($args->{'indentString'}, ""); $args->{'displayedHereMarker'} = 0; @@ -1725,16 +1789,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}; @@ -1760,7 +1823,7 @@ END } $result .= $colHTML . "\n"; } - $result .= " \n"; + $result .= &Apache::loncommon::end_data_table_row(); $args->{'isNewBranch'} = 0; } @@ -1779,6 +1842,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 @@ -1788,16 +1853,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 = ""; @@ -1813,37 +1875,62 @@ 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; } @@ -1933,6 +2020,14 @@ sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; + bless($self); # So we can call change_user if necessary + + $self->{USERNAME} = shift || $env{'user.name'}; + $self->{DOMAIN} = shift || $env{'user.domain'}; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; + + # 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. @@ -1942,38 +2037,105 @@ 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:domain + # Note: change_user is also called if we need the nav hash when printing CODEd + # assignments or printing an exam, in which the enclosing folder for the items in + # the exam has hidden set. + # - 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'}) && + !$self->{CODE} && !$self->{NOHIDE}) { + + # 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}, $self->{CODE}, $self->{NOHIDE}); } + + 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. +# code - Anonymous CODE in use. +# Implicit inputs: +# +sub change_user { + my $self = shift; + $self->{USERNAME} = shift; + $self->{DOMAIN} = shift; + $self->{CODE} = shift; + $self->{NOHIDE} = shift; + + # If the hashes are already tied make sure to break that bond: + + 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}, $self->{CODE}, $self->{NOHIDE}, \%big_hash); + $self->{NAV_HASH} = \%big_hash; + + + + # Now clear the parm cache and reconstruct the parm hash fromt he big_hash + # 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'}; @@ -2007,6 +2169,8 @@ sub generate_course_user_opt { return; } + + sub generate_email_discuss_status { my $self = shift; my $symb = shift; @@ -2016,7 +2180,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) ? @@ -2024,9 +2188,9 @@ 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) { + foreach my $key (keys(%lastread)) { my $shortkey = $key; $shortkey =~ s/_lastread$//; $lastreadtime{$shortkey} = $lastread{$key}; @@ -2034,8 +2198,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')) { @@ -2083,8 +2247,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; @@ -2313,7 +2477,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. @@ -2398,7 +2562,7 @@ sub parmval { my $self = shift; my ($what,$symb,$recurse)=@_; my $hashkey = $what."|||".$symb; - + my $cache = $self->{PARM_CACHE}; if (defined($self->{PARM_CACHE}->{$hashkey})) { if (ref($self->{PARM_CACHE}->{$hashkey}) eq 'ARRAY') { if (defined($self->{PARM_CACHE}->{$hashkey}->[0])) { @@ -2420,10 +2584,12 @@ sub parmval { return $result->[0]; } + sub parmval_real { my $self = shift; my ($what,$symb,$recurse) = @_; + # Make sure the {USER_OPT} and {COURSE_OPT} hashes are populated $self->generate_course_user_opt(); @@ -2435,8 +2601,8 @@ 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 ['']; } my $result=''; @@ -2452,18 +2618,23 @@ 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}; @@ -2530,6 +2701,217 @@ sub parmval_real { 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 @@ -2583,7 +2965,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 @@ -2838,6 +3220,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(); @@ -2951,13 +3336,11 @@ 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}); - + return Apache::lonnavmaps::iterator->new($self->{NAV_MAP}, $firstResource, + $finishResource, $self->{FILTER}, + $self->{ALREADY_SEEN}, + $self->{CONDITION}, + $self->{FORCE_TOP}); } # Set up some bookkeeping information. @@ -2976,7 +3359,6 @@ sub new { $self->{ALREADY_SEEN}->{$self->{FIRST_RESOURCE}->{ID}} = 1; bless ($self); - return $self; } @@ -2991,6 +3373,7 @@ 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}) { @@ -3010,7 +3393,6 @@ sub next { if ($self->{RECURSIVE_DEPTH} == 0) { $self->{RECURSIVE_ITERATOR_FLAG} = 0; } - return $next; } @@ -3086,6 +3468,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) { @@ -3119,7 +3503,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}, @@ -3211,6 +3594,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(); @@ -3447,10 +3833,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. @@ -3468,7 +3859,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 @@ -3706,15 +4101,42 @@ 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 =~ /\.(problem|exam|quiz|assess|survey|form|library|task)$/) { + if ($src =~ /$LONCAPA::assess_re/) { return 1; } return 0; @@ -3746,7 +4168,7 @@ sub is_survey { my $self = shift(); my $part = shift(); my $type = $self->parmval('type',$part); - if ($type eq 'survey') { + if (($type eq 'survey') || ($type eq 'surveycred')) { return 1; } if ($self->src() =~ /\.(survey)$/) { @@ -3754,6 +4176,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(); @@ -3807,6 +4238,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 @@ -3837,6 +4274,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 @@ -3867,7 +4309,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. @@ -3920,6 +4374,70 @@ Get the weight for the problem. =cut + + + +sub printable { + + my ($self, $part) = @_; + + # 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 (!@parts) { + return $self->printable(0); + } else { + foreach my $part (@parts) { + if (!$self->printable($part)) { + return 0; + } + } + return 1; + } +} + sub acc { (my $self, my $part) = @_; my $acc = $self->parmval("acc", $part); @@ -3942,9 +4460,46 @@ 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 $due_date=$self->parmval("duedate", $part); @@ -3961,6 +4516,7 @@ sub duedate { } else { $date = $due_date; } + $self->{DUEDATE_CACHE}->{$part} = $date; return $date; } sub handgrade { @@ -4022,8 +4578,8 @@ sub weight { my $self = shift; my $part = shift; if (!defined($part)) { $part = '0'; } my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', - $self->symb(), $env{'user.domain'}, - $env{'user.name'}, + $self->symb(), $self->{DOMAIN}, + $self->{USERNAME}, $env{'request.course.sec'}); return $weight; } @@ -4037,13 +4593,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; } } @@ -4310,7 +4874,7 @@ sub extractParts { } } } - my @sortedParts = sort keys %parts; + my @sortedParts = sort(keys(%parts)); $self->{PARTS} = \@sortedParts; } @@ -4331,13 +4895,13 @@ sub extractParts { # So we have to use our knowlege of part names to figure out # where the part names begin and end, and even then, it is possible # to construct ambiguous situations. - foreach my $data (split /,/, $metadata) { + foreach my $data (split(/,/, $metadata)) { if ($data =~ /^([a-zA-Z]+)response_(.*)/ || $data =~ /^(Task)_(.*)/) { my $responseType = $1; my $partStuff = $2; my $partIdSoFar = ''; - my @partChunks = split /_/, $partStuff; + my @partChunks = split(/_/, $partStuff); my $i = 0; for ($i = 0; $i < scalar(@partChunks); $i++) { if ($partIdSoFar) { $partIdSoFar .= '_'; } @@ -4469,7 +5033,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. @@ -4555,6 +5119,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 @@ -4566,6 +5134,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; @@ -4584,6 +5153,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; } @@ -4673,6 +5249,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. @@ -4681,9 +5261,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; @@ -4735,6 +5326,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; @@ -4773,7 +5368,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,$end,$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; } @@ -4825,6 +5519,7 @@ my %compositeToSimple = INCORRECT() => INCORRECT, OPEN() => OPEN, ATTEMPTED() => ATTEMPTED, + CREDIT_ATTEMPTED() => CORRECT, ANSWER_SUBMITTED() => ATTEMPTED ); @@ -4899,6 +5594,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; @@ -4949,7 +5645,7 @@ sub getPrevious { my $self = shift; my @branches; my $from = $self->from(); - foreach my $branch ( split /,/, $from) { + foreach my $branch ( split(/,/, $from)) { my $choice = $self->{NAV_MAP}->getById($branch); my $prev = $choice->comesfrom(); $prev = $self->{NAV_MAP}->getById($prev);