# The LearningOnline Network with CAPA # The LON-CAPA Grading handler # # $Id: grades.pm,v 1.530 2008/11/18 19:14:28 jms Exp $ # # Copyright Michigan State University Board of Trustees # # This file is part of the LearningOnline Network with CAPA (LON-CAPA). # # LON-CAPA is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # LON-CAPA is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with LON-CAPA; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # /home/httpd/html/adm/gpl.txt # # http://www.lon-capa.org/ # =head1 NAME Apache::grades =head1 SYNOPSIS Handles the viewing of grades. This is part of the LearningOnline Network with CAPA project described at http://www.lon-capa.org. =head1 OVERVIEW Do an ssi with retries: While I'd love to factor out this with the vesrion in lonprintout, that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure I'm not quite ready to invent (e.g. an ssi_with_retry object). At least the logic that drives this has been pulled out into loncommon. ssi_with_retries - Does the server side include of a resource. if the ssi call returns an error we'll retry it up to the number of times requested by the caller. If we still have a proble, no text is appended to the output and we set some global variables. to indicate to the caller an SSI error occurred. All of this is supposed to deal with the issues described in LonCAPA BZ 5631 see: http://bugs.lon-capa.org/show_bug.cgi?id=5631 by informing the user that this happened. Parameters: resource - The resource to include. This is passed directly, without interpretation to lonnet::ssi. form - The form hash parameters that guide the interpretation of the resource retries - Number of retries allowed before giving up completely. Returns: On success, returns the rendered resource identified by the resource parameter. Side Effects: The following global variables can be set: ssi_error - If an unrecoverable error occurred this becomes true. It is up to the caller to initialize this to false if desired. ssi_error_resource - If an unrecoverable error occurred, this is the value of the resource that could not be rendered by the ssi call. ssi_error_message - The error string fetched from the ssi response in the event of an error. =head1 HANDLER SUBROUTINE ssi_with_retries() =head1 SUBROUTINES =over =item scantron_get_correction() : Builds the interface screen to interact with the operator to fix a specific error condition in a specific scanline Arguments: $r - Apache request object $i - number of the current scanline $scan_record - hash ref as returned from &scantron_parse_scanline() $scan_config - hash ref as returned from &get_scantron_config() $line - full contents of the current scanline $error - error condition, valid values are 'incorrectCODE', 'duplicateCODE', 'doublebubble', 'missingbubble', 'duplicateID', 'incorrectID' $arg - extra information needed For errors: - duplicateID - paper number that this studentID was seen before on - duplicateCODE - array ref of the paper numbers this CODE was seen on before - incorrectCODE - current incorrect CODE - doublebubble - array ref of the bubble lines that have double bubble errors - missingbubble - array ref of the bubble lines that have missing bubble errors =item scantron_get_maxbubble() : Returns the maximum number of bubble lines that are expected to occur. Does this by walking the selected sequence rendering the resource and then checking &Apache::lonxml::get_problem_counter() for what the current value of the problem counter is. Caches the results to $env{'form.scantron_maxbubble'}, $env{'form.scantron.bubble_lines.n'}, $env{'form.scantron.first_bubble_line.n'} and $env{"form.scantron.sub_bubblelines.n"} which are the total number of bubble, lines, the number of bubble lines for response n and number of the first bubble line for response n, and a comma separated list of numbers of bubble lines for sub-questions (for optionresponse, matchresponse, and rankresponse items), for response n. =item scantron_validate_missingbubbles() : Validates all scanlines in the selected file to not have any answers that don't have bubbles that have not been verified to be bubble free. =item scantron_process_students() : Routine that does the actual grading of the bubble sheet information. The parsed scanline hash is added to %env Then foreach unskipped scanline it does an &Apache::lonnet::ssi() foreach resource , with the form data of 'submitted' =>'scantron' 'grade_target' =>'grade', 'grade_username'=> username of student 'grade_domain' => domain of student 'grade_courseid'=> of course 'grade_symb' => symb of resource to grade This triggers a grading pass. The problem grading code takes care of converting the bubbled letter information (now in %env) into a valid submission. =item scantron_upload_scantron_data() : Creates the screen for adding a new bubble sheet data file to a course. =item scantron_upload_scantron_data_save() : Adds a provided bubble information data file to the course if user has the correct privileges to do so. =item valid_file() : Validates that the requested bubble data file exists in the course. =item scantron_download_scantron_data() : Shows a list of the three internal files (original, corrected, skipped) for a specific bubble sheet data file that exists in the course. =item scantron_validate_ID() : Validates all scanlines in the selected file to not have any invalid or underspecified student IDs =back =cut package Apache::grades; use strict; use Apache::style; use Apache::lonxml; use Apache::lonnet; use Apache::loncommon; use Apache::lonhtmlcommon; use Apache::lonnavmaps; use Apache::lonhomework; use Apache::lonpickcode; use Apache::loncoursedata; use Apache::lonmsg(); use Apache::Constants qw(:common); use Apache::lonlocal; use Apache::lonenc; use String::Similarity; use LONCAPA; use POSIX qw(floor); my %perm=(); # These variables are used to recover from ssi errors my $ssi_retries = 5; my $ssi_error; my $ssi_error_resource; my $ssi_error_message; sub ssi_with_retries { my ($resource, $retries, %form) = @_; my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form); if ($response->is_error) { $ssi_error = 1; $ssi_error_resource = $resource; $ssi_error_message = $response->code . " " . $response->message; } return $content; } # # Prodcuces an ssi retry failure error message to the user: # sub ssi_print_error { my ($r) = @_; my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk'); $r->print('

'.&mt('An unrecoverable network error occurred:').'

'.&mt('Unable to retrieve a resource from a server:').'
'.&mt('Resource:').' '.$ssi_error_resource.'
'.&mt('Error:').' '.$ssi_error_message.'

'. &mt('It is recommended that you try again later, as this error may mean the server was just temporarily unavailable, or is down for maintenance.').'
'. &mt('If the error persists, please contact the [_1] for assistance.',$helpurl). '

'); return; } # # --- Retrieve the parts from the metadata file.--- sub getpartlist { my ($symb) = @_; my $navmap = Apache::lonnavmaps::navmap->new(); my $res = $navmap->getBySymb($symb); my $partlist = $res->parts(); my $url = $res->src(); my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys')); my @stores; foreach my $part (@{ $partlist }) { foreach my $key (@metakeys) { if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); } } } return @stores; } # --- Get the symbolic name of a problem and the url sub get_symb { my ($request,$silent) = @_; (my $url=$env{'form.url'}) =~ s-^http://($ENV{'SERVER_NAME'}|$ENV{'HTTP_HOST'})--; my $symb=($env{'form.symb'} ne '' ? $env{'form.symb'} : (&Apache::lonnet::symbread($url))); if ($symb eq '') { if (!$silent) { $request->print("Unable to handle ambiguous references:$url:."); return (); } } &Apache::lonenc::check_decrypt(\$symb); return ($symb); } #--- Format fullname, username:domain if different for display #--- Use anywhere where the student names are listed sub nameUserString { my ($type,$fullname,$uname,$udom) = @_; if ($type eq 'header') { return ' '.&mt('Fullname').' ('.&mt('Username').')'; } else { return ' '.$fullname.' ('.$uname. ($env{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')'; } } #--- Get the partlist and the response type for a given problem. --- #--- Indicate if a response type is coded handgraded or not. --- sub response_type { my ($symb) = shift; my $navmap = Apache::lonnavmaps::navmap->new(); my $res = $navmap->getBySymb($symb); my $partlist = $res->parts(); my %vPart = map { $_ => 1 } (&Apache::loncommon::get_env_multiple('form.vPart')); my (%response_types,%handgrade); foreach my $part (@{ $partlist }) { next if (%vPart && !exists($vPart{$part})); my @types = $res->responseType($part); my @ids = $res->responseIds($part); for (my $i=0; $i < scalar(@ids); $i++) { $response_types{$part}{$ids[$i]} = $types[$i]; $handgrade{$part.'_'.$ids[$i]} = &Apache::lonnet::EXT('resource.'.$part.'_'.$ids[$i]. '.handgrade',$symb); } } return ($partlist,\%handgrade,\%response_types); } sub flatten_responseType { my ($responseType) = @_; my @part_response_id = map { my $part = $_; map { [$part,$_] } sort(keys(%{ $responseType->{$part} })); } sort(keys(%$responseType)); return @part_response_id; } sub get_display_part { my ($partID,$symb)=@_; my $display=&Apache::lonnet::EXT('resource.'.$partID.'.display',$symb); if (defined($display) and $display ne '') { $display.= " (id $partID)"; } else { $display=$partID; } return $display; } #--- Show resource title #--- and parts and response type sub showResourceInfo { my ($symb,$probTitle,$checkboxes) = @_; my $col=3; if ($checkboxes) { $col=4; } my $result = '

'.&mt('Current Resource').': '.$probTitle.'

'."\n"; $result .=''; my ($partlist,$handgrade,$responseType) = &response_type($symb); my %resptype = (); my $hdgrade='no'; my %partsseen; foreach my $partID (sort(keys(%$responseType))) { foreach my $resID (sort(keys(%{ $responseType->{$partID} }))) { my $handgrade=$$handgrade{$partID.'_'.$resID}; my $responsetype = $responseType->{$partID}->{$resID}; $hdgrade = $handgrade if ($handgrade eq 'yes'); $result.=''; if ($checkboxes) { if (exists($partsseen{$partID})) { $result.=""; } else { $result.=""; } $partsseen{$partID}=1; } my $display_part=&get_display_part($partID,$symb); $result.=''. ''; # ''; } } $result.='
 '.&mt('Part: [_1]',$display_part).' '. $resID.''.&mt('Type: [_1]',$responsetype).'
'.&mt('Handgrade: [_1]',$handgrade).'
'."\n"; return $result,$responseType,$hdgrade,$partlist,$handgrade; } sub reset_caches { &reset_analyze_cache(); &reset_perm(); } { my %analyze_cache; sub reset_analyze_cache { undef(%analyze_cache); } sub get_analyze { my ($symb,$uname,$udom,$no_increment)=@_; my $key = "$symb\0$uname\0$udom"; return $analyze_cache{$key} if (exists($analyze_cache{$key})); my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb); $url=&Apache::lonnet::clutter($url); my $subresult=&ssi_with_retries($url, $ssi_retries, ('grade_target' => 'analyze', 'grade_domain' => $udom, 'grade_symb' => $symb, 'grade_courseid' => $env{'request.course.id'}, 'grade_username' => $uname, 'grade_noincrement' => $no_increment)); (undef,$subresult)=split(/_HASH_REF__/,$subresult,2); my %analyze=&Apache::lonnet::str2hash($subresult); return $analyze_cache{$key} = \%analyze; } sub get_order { my ($partid,$respid,$symb,$uname,$udom,$no_increment)=@_; my $analyze = &get_analyze($symb,$uname,$udom,$no_increment); return $analyze->{"$partid.$respid.shown"}; } sub get_radiobutton_correct_foil { my ($partid,$respid,$symb,$uname,$udom)=@_; my $analyze = &get_analyze($symb,$uname,$udom); foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) { if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') { return $foil; } } } } #--- Clean response type for display #--- Currently filters option/rank/radiobutton/match/essay/Task # response types only. sub cleanRecord { my ($answer,$response,$symb,$partid,$respid,$record,$order,$version, $uname,$udom) = @_; my $grayFont = ''; if ($response =~ /^(option|rank)$/) { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my ($toprow,$bottomrow); foreach my $foil (@$order) { if ($grading{$foil} == 1) { $toprow.=''.$answer{$foil}.' '; } else { $toprow.=''.$answer{$foil}.' '; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $grayFont.$bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Option ID').'
'; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); my ($toprow,$middlerow,$bottomrow); foreach my $foil (@$order) { my $item=shift(@items); if ($grading{$foil} == 1) { $toprow.=''.$item.' '; $middlerow.=''.$grayFont.$answer{$foil}.' '; } else { $toprow.=''.$item.' '; $middlerow.=''.$grayFont.$answer{$foil}.' '; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $middlerow.''. ''. $bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Item ID').'
'.$grayFont.&mt('Option ID').'
'; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); my ($toprow,$bottomrow); my $correct = &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); foreach my $foil (@$order) { if (exists($answer{$foil})) { if ($foil eq $correct) { $toprow.=''.&mt('true').''; } else { $toprow.=''.&mt('true').''; } } else { $toprow.=''.&mt('false').''; } $bottomrow.=''.$grayFont.$foil.' '; } return '
'. ''.$toprow.''. ''. $grayFont.$bottomrow.''.'
'.&mt('Answer').'
'.$grayFont.&mt('Option ID').'
'; } elsif ($response eq 'essay') { if (! exists ($env{'form.'.$symb})) { my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. } $answer =~ s-\n-
-g; return '

'.&keywords_highlight($answer).'
'; } elsif ( $response eq 'organic') { my $result='Smile representation: "'.$answer.'"'; my $jme=$record->{$version."resource.$partid.$respid.molecule"}; $result.=&Apache::chemresponse::jme_img($jme,$answer,400); return $result; } elsif ( $response eq 'Task') { if ( $answer eq 'SUBMITTED') { my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"}; my $result = &Apache::bridgetask::file_list($files,$uname,$udom); return $result; } elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) { my @matches = grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})); return join('
',($version,@matches)); } else { my $result = '

' .&mt('Overall result: [_1]', $record->{$version."resource.$respid.$partid.status"}) .'

'; $result .= ''; return $result; } } elsif ( $response =~ m/(?:numerical|formula)/) { $answer = &Apache::loncommon::format_previous_attempt_value('submission', $answer); } return $answer; } #-- A couple of common js functions sub commonJSfunctions { my $request = shift; $request->print(< function radioSelection(radioButton) { var selection=null; if (radioButton.length > 1) { for (var i=0; i 1) { for (var i=0; i COMMONJSFUNCTIONS } #--- Dumps the class list with usernames,list of sections, #--- section, ids and fullnames for each user. sub getclasslist { my ($getsec,$filterlist,$getgroup) = @_; my @getsec; my @getgroup; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); if (!ref($getsec)) { if ($getsec ne '' && $getsec ne 'all') { @getsec=($getsec); } } else { @getsec=@{$getsec}; } if (grep(/^all$/,@getsec)) { undef(@getsec); } if (!ref($getgroup)) { if ($getgroup ne '' && $getgroup ne 'all') { @getgroup=($getgroup); } } else { @getgroup=@{$getgroup}; } if (grep(/^all$/,@getgroup)) { undef(@getgroup); } my ($classlist,$keylist)=&Apache::loncoursedata::get_classlist(); # Bail out if we were unable to get the classlist return if (! defined($classlist)); &Apache::loncoursedata::get_group_memberships($classlist,$keylist); # my %sections; my %fullnames; foreach my $student (keys(%$classlist)) { my $end = $classlist->{$student}->[&Apache::loncoursedata::CL_END()]; my $start = $classlist->{$student}->[&Apache::loncoursedata::CL_START()]; my $id = $classlist->{$student}->[&Apache::loncoursedata::CL_ID()]; my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; my $fullname = $classlist->{$student}->[&Apache::loncoursedata::CL_FULLNAME()]; my $status = $classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()]; my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; # filter students according to status selected if ($filterlist && (!($stu_status =~ /Any/))) { if (!($stu_status =~ $status)) { delete($classlist->{$student}); next; } } # filter students according to groups selected my @stu_groups = split(/,/,$group); if (@getgroup) { my $exclude = 1; foreach my $grp (@getgroup) { foreach my $stu_group (@stu_groups) { if ($stu_group eq $grp) { $exclude = 0; } } if (($grp eq 'none') && !$group) { $exclude = 0; } } if ($exclude) { delete($classlist->{$student}); } } $section = ($section ne '' ? $section : 'none'); if (&canview($section)) { if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { $sections{$section}++; if ($classlist->{$student}) { $fullnames{$student}=$fullname; } } else { delete($classlist->{$student}); } } else { delete($classlist->{$student}); } } my %seen = (); my @sections = sort(keys(%sections)); return ($classlist,\@sections,\%fullnames); } sub canmodify { my ($sec)=@_; if ($perm{'mgr'}) { if (!defined($perm{'mgr_section'})) { # can modify whole class return 1; } else { if ($sec eq $perm{'mgr_section'}) { #can modify the requested section return 1; } else { # can't modify the request section return 0; } } } #can't modify return 0; } sub canview { my ($sec)=@_; if ($perm{'vgr'}) { if (!defined($perm{'vgr_section'})) { # can modify whole class return 1; } else { if ($sec eq $perm{'vgr_section'}) { #can modify the requested section return 1; } else { # can't modify the request section return 0; } } } #can't modify return 0; } #--- Retrieve the grade status of a student for all the parts sub student_gradeStatus { my ($symb,$udom,$uname,$partlist) = @_; my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$udom,$uname); my %partstatus = (); foreach (@$partlist) { my ($status,undef) = split(/_/,$record{"resource.$_.solved"},2); $status = 'nothing' if ($status eq ''); $partstatus{$_} = $status; my $subkey = "resource.$_.submitted_by"; $partstatus{$subkey} = $record{$subkey} if ($record{$subkey} ne ''); } return %partstatus; } # hidden form and javascript that calls the form # Use by verifyscript and viewgrades # Shows a student's view of problem and submission sub jscriptNform { my ($symb) = @_; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $jscript=''."\n"; $jscript.= '
'."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". ''."\n". '
'."\n"; return $jscript; } # Given the score (as a number [0-1] and the weight) what is the final # point value? This function will round to the nearest tenth, third, # or quarter if one of those is within the tolerance of .00001. sub compute_points { my ($score, $weight) = @_; my $tolerance = .00001; my $points = $score * $weight; # Check for nearness to 1/x. my $check_for_nearness = sub { my ($factor) = @_; my $num = ($points * $factor) + $tolerance; my $floored_num = floor($num); if ($num - $floored_num < 2 * $tolerance * $factor) { return $floored_num / $factor; } return $points; }; $points = $check_for_nearness->(10); $points = $check_for_nearness->(3); $points = $check_for_nearness->(4); return $points; } #------------------ End of general use routines -------------------- # # Find most similar essay # sub most_similar { my ($uname,$udom,$uessay,$old_essays)=@_; # ignore spaces and punctuation $uessay=~s/\W+/ /gs; # ignore empty submissions (occuring when only files are sent) unless ($uessay=~/\w+/) { return ''; } # these will be returned. Do not care if not at least 50 percent similar my $limit=0.6; my $sname=''; my $sdom=''; my $scrsid=''; my $sessay=''; # go through all essays ... foreach my $tkey (keys(%$old_essays)) { my ($tname,$tdom,$tcrsid)=map {&unescape($_)} (split(/\./,$tkey)); # ... except the same student next if (($tname eq $uname) && ($tdom eq $udom)); my $tessay=$old_essays->{$tkey}; $tessay=~s/\W+/ /gs; # String similarity gives up if not even limit my $tsimilar=&String::Similarity::similarity($uessay,$tessay,$limit); # Found one if ($tsimilar>$limit) { $limit=$tsimilar; $sname=$tname; $sdom=$tdom; $scrsid=$tcrsid; $sessay=$old_essays->{$tkey}; } } if ($limit>0.6) { return ($sname,$sdom,$scrsid,$sessay,$limit); } else { return ('','','','',0); } } #------------------------------------------------------------------- #------------------------------------ Receipt Verification Routines # #--- Check whether a receipt number is valid.--- sub verifyreceipt { my $request = shift; my $courseid = $env{'request.course.id'}; my $receipt = &Apache::lonnet::recprefix($courseid).'-'. $env{'form.receipt'}; $receipt =~ s/[^\-\d]//g; my ($symb) = &get_symb($request); my $title.= '

'. &mt('Verifying Submission Receipt [_1]',$receipt). '

'."\n". '

'.&mt('Resource: [_1]',$env{'form.probTitle'}). '

'."\n"; my ($string,$contents,$matches) = ('','',0); my (undef,undef,$fullname) = &getclasslist('all','0'); my $receiptparts=0; if ($env{"course.$courseid.receiptalg"} eq 'receipt2' || $env{"course.$courseid.receiptalg"} eq 'receipt3') { $receiptparts=1; } my $parts=['0']; if ($receiptparts) { ($parts)=&response_type($symb); } my $header = &Apache::loncommon::start_data_table(). &Apache::loncommon::start_data_table_header_row(). ' '.&mt('Fullname').' '."\n". ' '.&mt('Username').' '."\n". ' '.&mt('Domain').' '; if ($receiptparts) { $header.=' '.&mt('Problem Part').' '; } $header.= &Apache::loncommon::end_data_table_header_row(); foreach (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom)=split(/\:/); foreach my $part (@$parts) { if ($receipt eq &Apache::lonnet::ireceipt($uname,$udom,$courseid,$symb,$part)) { $contents.= &Apache::loncommon::start_data_table_row(). ' '."\n". ''.$$fullname{$_}.' '."\n". ' '.$uname.' '. ' '.$udom.' '; if ($receiptparts) { $contents.=' '.$part.' '; } $contents.= &Apache::loncommon::end_data_table_row()."\n"; $matches++; } } } if ($matches == 0) { $string = $title.&mt('No match found for the above receipt.'); } else { $string = &jscriptNform($symb).$title. '

'. &mt('The above receipt matches the following [numerate,_1,student].',$matches). '

'. $header. $contents. &Apache::loncommon::end_data_table()."\n"; } return $string.&show_grading_menu_form($symb); } #--- This is called by a number of programs. #--- Called from the Grading Menu - View/Grade an individual student #--- Also called directly when one clicks on the subm button # on the problem page. sub listStudents { my ($request) = shift; my ($symb) = &get_symb($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'}; my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; my $result='

 '. &mt($viewgrade.' Submissions for a Student or a Group of Students') .'

'; my ($table,undef,$hdgrade,$partlist,$handgrade) = &showResourceInfo($symb,$env{'form.probTitle'},($env{'form.showgrading'} eq 'yes')); my %lt = ( 'multiple' => "Please select a student or group of students before clicking on the Next button.", 'single' => "Please select the student before clicking on the Next button.", ); %lt = &Apache::lonlocal::texthash(%lt); $request->print(< function checkSelect(checkBox) { var ctr=0; var sense=""; if (checkBox.length > 1) { for (var i=0; i LISTJAVASCRIPT &commonJSfunctions($request); $request->print($result); my $checkhdgrade = ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1 ) ? 'checked="checked"' : ''; my $checklastsub = $checkhdgrade eq '' ? 'checked="checked"' : ''; my $gradeTable='
'. "\n".$table; $gradeTable .= ' '. &mt('View Problem Text: [_1]', ''."\n". ''."\n". '').'
'."\n"; $gradeTable .= ' '. &mt('View Answer: [_1]', ''."\n". ''."\n". '').'
'."\n"; my $submission_options; if ($env{'form.handgrade'} eq 'yes' && scalar(@$partlist) > 1) { $submission_options.= ''."\n"; } my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); my $saveStatus = $stu_status eq '' ? 'Active' : $stu_status; $env{'form.Status'} = $saveStatus; $submission_options.= ''."\n". ''."\n". ''."\n". ''; $gradeTable .= ' '. &mt('Submissions: [_1]',$submission_options).'
'."\n"; $gradeTable .= ' '. &mt('Grading Increments: [_1]', ''); $gradeTable .= &build_section_inputs(). ''."\n". '
'."\n". '
'."\n". ''."\n". ''."\n". ''."\n". ''."\n"; if (exists($env{'form.gradingMenu'}) && exists($env{'form.Status'})) { $gradeTable.=''."\n"; } else { $gradeTable.=&mt('Student Status: [_1]', &Apache::lonhtmlcommon::StatusOptions($saveStatus,undef,1,'javascript:reLoadList(this.form);')).'
'; } $gradeTable.=&mt('To '.lc($viewgrade).' a submission or a group of submissions, click on the check box(es) '. 'next to the student\'s name(s). Then click on the Next button.').'
'."\n". ''."\n"; # checkall buttons $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='
'."\n"; $gradeTable.=&check_buttons(); $gradeTable.=''; my ($classlist, undef, $fullname) = &getclasslist($getsec,'1',$getgroup); $gradeTable.= &Apache::loncommon::start_data_table(). &Apache::loncommon::start_data_table_header_row(); my $loop = 0; while ($loop < 2) { $gradeTable.=''.&mt('No.').''.&mt('Select').''. ''.&nameUserString('header').' '.&mt('Section/Group').''; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { foreach my $part (sort(@$partlist)) { my $display_part= &get_display_part((split(/_/,$part))[0],$symb); $gradeTable.= ''.&mt('Part: [_1] Status',$display_part).''; } } elsif ($submitonly eq 'queued') { $gradeTable.=''.&mt('Queue Status').' '; } $loop++; # $gradeTable.='' if ($loop%2 ==1); } $gradeTable.=&Apache::loncommon::end_data_table_header_row()."\n"; my $ctr = 0; foreach my $student (sort { if (lc($$fullname{$a}) ne lc($$fullname{$b})) { return (lc($$fullname{$a}) cmp lc($$fullname{$b})); } return $a cmp $b; } (keys(%$fullname))) { my ($uname,$udom) = split(/:/,$student); my %status = (); if ($submitonly eq 'queued') { my %queue_status = &Apache::bridgetask::get_student_status($symb,$cdom,$cnum, $udom,$uname); next if (!defined($queue_status{'gradingqueue'})); $status{'gradingqueue'} = $queue_status{'gradingqueue'}; } if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { (%status) =&student_gradeStatus($symb,$udom,$uname,$partlist); my $submitted = 0; my $graded = 0; my $incorrect = 0; foreach (keys(%status)) { $submitted = 1 if ($status{$_} ne 'nothing'); $graded = 1 if ($status{$_} =~ /^ungraded/); $incorrect = 1 if ($status{$_} =~ /^incorrect/); my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; my ($part)=split(/\./,$partid); $gradeTable.=''; } } next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'incorrect' || $submitonly eq 'graded')); next if (!$graded && ($submitonly eq 'graded')); next if (!$incorrect && $submitonly eq 'incorrect'); } $ctr++; my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()]; if ( $perm{'vgr'} eq 'F' ) { if ($ctr%2 ==1) { $gradeTable.= &Apache::loncommon::start_data_table_row(); } $gradeTable.=''.$ctr.' '. ''."\n".''. &nameUserString(undef,$$fullname{$student},$uname,$udom). ' '.$section.($group ne '' ?'/'.$group:'').''."\n"; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { foreach (sort(keys(%status))) { next if ($_ =~ /^resource.*?submitted_by$/); $gradeTable.=' '.&mt($status{$_}).' '."\n"; } } # $gradeTable.='' if ($ctr%2 ==1); if ($ctr%2 ==0) { $gradeTable.=&Apache::loncommon::end_data_table_row()."\n"; } } } if ($ctr%2 ==1) { $gradeTable.='   '; if ($env{'form.showgrading'} eq 'yes' && $submitonly ne 'queued' && $submitonly ne 'all') { foreach (@$partlist) { $gradeTable.=' '; } } elsif ($submitonly eq 'queued') { $gradeTable.=' '; } $gradeTable.=&Apache::loncommon::end_data_table_row(); } $gradeTable.=&Apache::loncommon::end_data_table()."\n". '
'."\n"; if ($ctr == 0) { my $num_students=(scalar(keys(%$fullname))); if ($num_students eq 0) { $gradeTable='
 '.&mt('There are no students currently enrolled.').''; } else { my $submissions='submissions'; if ($submitonly eq 'incorrect') { $submissions = 'incorrect submissions'; } if ($submitonly eq 'graded' ) { $submissions = 'ungraded submissions'; } if ($submitonly eq 'queued' ) { $submissions = 'queued submissions'; } $gradeTable='
 '. &mt('No '.$submissions.' found for this resource for any students. ([_1] students checked for '.$submissions.')', $num_students). '
'; } } elsif ($ctr == 1) { $gradeTable =~ s/type="checkbox"/type="checkbox" checked="checked"/; } $gradeTable.=&show_grading_menu_form($symb); $request->print($gradeTable); return ''; } #---- Called from the listStudents routine sub check_script { my ($form, $type)=@_; my $chkallscript=''."\n"; return $chkallscript; } sub check_buttons { my $buttons.=''; $buttons.=' '; $buttons.=''; $buttons.=' '; return $buttons; } # Displays the submissions for one student or a group of students sub processGroup { my ($request) = shift; my $ctr = 0; my @stuchecked = &Apache::loncommon::get_env_multiple('form.stuinfo'); my $total = scalar(@stuchecked)-1; foreach my $student (@stuchecked) { my ($uname,$udom,$fullname) = split(/:/,$student); $env{'form.student'} = $uname; $env{'form.userdom'} = $udom; $env{'form.fullname'} = $fullname; &submission($request,$ctr,$total); $ctr++; } return ''; } #------------------------------------------------------------------------------------ # #-------------------------- Next few routines handles grading by student, essentially # handles essay response type problem/part # #--- Javascript to handle the submission page functionality --- sub sub_page_js { my $request = shift; $request->print(< function updateRadio(formname,id,weight) { var gradeBox = formname["GD_BOX"+id]; var radioButton = formname["RADVAL"+id]; var oldpts = formname["oldpts"+id].value; var pts = checkSolved(formname,id) == 'update' ? gradeBox.value : oldpts; gradeBox.value = pts; var resetbox = false; if (isNaN(pts) || pts < 0) { alert("A number equal or greater than 0 is expected. Entered value = "+pts); for (var i=0; i weight) { var resp = confirm("You entered a value ("+pts+ ") greater than the weight for the part. Accept?"); if (resp == false) { gradeBox.value = oldpts; return; } } for (var i=0; i SUBJAVASCRIPT } #--- javascript for essay type problem -- sub sub_page_kw_js { my $request = shift; my $iconpath = $request->dir_config('lonIconsURL'); &commonJSfunctions($request); my $inner_js_msg_central=< function checkInput() { opener.document.SCORE.msgsub.value = opener.checkEntities(document.msgcenter.msgsub.value); var nmsg = opener.document.SCORE.savemsgN.value; var usrctr = document.msgcenter.usrctr.value; var newval = opener.document.SCORE["newmsg"+usrctr]; newval.value = opener.checkEntities(document.msgcenter.newmsg.value); var msgchk = ""; if (document.msgcenter.subchk.checked) { msgchk = "msgsub,"; } var includemsg = 0; for (var i=1; i<=nmsg; i++) { var opnmsg = opener.document.SCORE["savemsg"+i]; var frmmsg = document.msgcenter["msg"+i]; opnmsg.value = opener.checkEntities(frmmsg.value); var showflg = opener.document.SCORE["shownOnce"+i]; showflg.value = "1"; var chkbox = document.msgcenter["msgn"+i]; if (chkbox.checked) { msgchk += "savemsg"+i+","; includemsg = 1; } } if (document.msgcenter.newmsgchk.checked) { msgchk += "newmsg"+usrctr; includemsg = 1; } imgformname = opener.document.SCORE["mailicon"+usrctr]; imgformname.src = "$iconpath/"+((includemsg) ? "mailto.gif" : "mailbkgrd.gif"); var includemsg = opener.document.SCORE["includemsg"+usrctr]; includemsg.value = msgchk; self.close() } INNERJS my $inner_js_highlight_central=< function updateChoice(flag) { opener.document.SCORE.kwclr.value = opener.radioSelection(document.hlCenter.kwdclr); opener.document.SCORE.kwsize.value = opener.radioSelection(document.hlCenter.kwdsize); opener.document.SCORE.kwstyle.value = opener.radioSelection(document.hlCenter.kwdstyle); opener.document.SCORE.refresh.value = "on"; if (opener.document.SCORE.keywords.value!=""){ opener.document.SCORE.submit(); } self.close() } INNERJS my $start_page_msg_central = &Apache::loncommon::start_page('Message Central',$inner_js_msg_central, {'js_ready' => 1, 'only_body' => 1, 'bgcolor' =>'#FFFFFF',}); my $end_page_msg_central = &Apache::loncommon::end_page({'js_ready' => 1}); my $start_page_highlight_central = &Apache::loncommon::start_page('Highlight Central', $inner_js_highlight_central, {'js_ready' => 1, 'only_body' => 1, 'bgcolor' =>'#FFFFFF',}); my $end_page_highlight_central = &Apache::loncommon::end_page({'js_ready' => 1}); my $docopen=&Apache::lonhtmlcommon::javascript_docopen(); $docopen=~s/^document\.//; $request->print(< //===================== Show list of keywords ==================== function keywords(formname) { var nret = prompt("Keywords list, separated by a space. Add/delete to list if desired.",formname.keywords.value); if (nret==null) return; formname.keywords.value = nret; if (formname.keywords.value != "") { formname.refresh.value = "on"; formname.submit(); } return; } //===================== Script to view submitted by ================== function viewSubmitter(submitter) { document.SCORE.refresh.value = "on"; document.SCORE.NCT.value = "1"; document.SCORE.unamedom0.value = submitter; document.SCORE.submit(); return; } //===================== Script to add keyword(s) ================== function getSel() { if (document.getSelection) txt = document.getSelection(); else if (document.selection) txt = document.selection.createRange().text; else return; var cleantxt = txt.replace(new RegExp('([\\f\\n\\r\\t\\v ])+', 'g')," "); if (cleantxt=="") { alert("Please select a word or group of words from document and then click this link."); return; } var nret = prompt("Add selection to keyword list? Edit if desired.",cleantxt); if (nret==null) return; document.SCORE.keywords.value = document.SCORE.keywords.value+" "+nret; if (document.SCORE.keywords.value != "") { document.SCORE.refresh.value = "on"; document.SCORE.submit(); } return; } //====================== Script for composing message ============== // preload images img1 = new Image(); img1.src = "$iconpath/mailbkgrd.gif"; img2 = new Image(); img2.src = "$iconpath/mailto.gif"; function msgCenter(msgform,usrctr,fullname) { var Nmsg = msgform.savemsgN.value; savedMsgHeader(Nmsg,usrctr,fullname); var subject = msgform.msgsub.value; var msgchk = document.SCORE["includemsg"+usrctr].value; re = /msgsub/; var shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } subject = (document.SCORE.shownSub.value == 0 ? checkEntities(subject) : subject); displaySubject(checkEntities(subject),shwsel); for (var i=1; i<=Nmsg; i++) { var testmsg = "savemsg"+i+","; re = new RegExp(testmsg,"g"); shwsel = ""; if (re.test(msgchk)) { shwsel = "checked" } var message = document.SCORE["savemsg"+i].value; message = (document.SCORE["shownOnce"+i].value == 0 ? checkEntities(message) : message); displaySavedMsg(i,message,shwsel); //I do not get it. w/o checkEntities on saved messages, //any < is already converted to <, etc. However, only once!! } newmsg = document.SCORE["newmsg"+usrctr].value; shwsel = ""; re = /newmsg/; if (re.test(msgchk)) { shwsel = "checked" } newMsg(newmsg,shwsel); msgTail(); return; } function checkEntities(strx) { if (strx.length == 0) return strx; var orgStr = ["&", "<", ">", '"']; var newStr = ["&", "<", ">", """]; var counter = 0; while (counter < 4) { strx = strReplace(strx,orgStr[counter],newStr[counter]); counter++; } return strx; } function strReplace(strx, orgStr, newStr) { return strx.split(orgStr).join(newStr); } function savedMsgHeader(Nmsg,usrctr,fullname) { var height = 70*Nmsg+250; var scrollbar = "no"; if (height > 600) { height = 600; scrollbar = "yes"; } var xpos = (screen.width-600)/2; xpos = (xpos < 0) ? '0' : xpos; var ypos = (screen.height-height)/2-30; ypos = (ypos < 0) ? '0' : ypos; pWin = window.open('', 'MessageCenter', 'resizable=yes,toolbar=no,location=no,scrollbars='+scrollbar+',screenx='+xpos+',screeny='+ypos+',width=600,height='+height); pWin.focus(); pDoc = pWin.document; pDoc.$docopen; pDoc.write('$start_page_msg_central'); pDoc.write("
"); pDoc.write(""); pDoc.write("

 Compose Message for \"+fullname+\"<\\/span><\\/h3>

"); pDoc.write("
"); pDoc.write(""); pDoc.write(""); pDoc.write(""); pDoc.write("
Type<\\/b><\\/td>Include<\\/b><\\/td>Message<\\/td><\\/tr>"); } function displaySubject(msg,shwsel) { pDoc = pWin.document; pDoc.write("
Subject<\\/td>"); pDoc.write("<\\/td>"); pDoc.write("<\\/td><\\/tr>"); } function displaySavedMsg(ctr,msg,shwsel) { pDoc = pWin.document; pDoc.write("
"+ctr+"<\\/td>"); pDoc.write("<\\/td>"); pDoc.write("