--- loncom/homework/grades.pm 2005/02/07 17:07:58 1.242 +++ loncom/homework/grades.pm 2005/04/02 18:44:39 1.254 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.242 2005/02/07 17:07:58 albertel Exp $ +# $Id: grades.pm,v 1.254 2005/04/02 18:44:39 banghart Exp $ # # Copyright Michigan State University Board of Trustees # @@ -96,7 +96,7 @@ sub get_symb_and_url { sub nameUserString { my ($type,$fullname,$uname,$udom) = @_; if ($type eq 'header') { - return '<b> Fullname </b><font color="#999999">(Username)</font> '; + return '<b> Fullname </b><font color="#999999">(Username)</font>'; } else { return ' '.$fullname.'<font color="#999999"> ('.$uname. ($ENV{'user.domain'} eq $udom ? '' : ' ('.$udom.')').')</font>'; @@ -664,17 +664,21 @@ LISTJAVASCRIPT $gradeTable.='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.<br />'."\n". '<input type="hidden" name="command" value="processGroup" />'."\n"; + +# checkall buttons + $gradeTable.=&check_script('gradesub', 'stuinfo'); $gradeTable.='<input type="button" '."\n". 'onClick="javascript:checkSelect(this.form.stuinfo);" '."\n". - 'value="Next->" />'."\n"; + 'value="Next->" /> <br />'."\n"; + $gradeTable.=&check_buttons(); $gradeTable.='<input type="checkbox" name="checkPlag" checked="on">Check For Plagiarism</input>'; - my (undef, undef, $fullname) = &getclasslist($getsec,'1'); + my ($classlist, undef, $fullname) = &getclasslist($getsec,'1'); $gradeTable.='<table border="0"><tr><td bgcolor="#777777">'. '<table border="0"><tr bgcolor="#e6ffff">'; my $loop = 0; while ($loop < 2) { $gradeTable.='<td><b> No.</b> </td><td><b> Select </b></td>'. - '<td>'.&nameUserString('header').'</td>'; + '<td>'.&nameUserString('header').' Section/Group</td>'; if ($ENV{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { foreach (sort(@$partlist)) { my $display_part=&get_display_part((split(/_/))[0],$url,$symb); @@ -695,10 +699,12 @@ LISTJAVASCRIPT (%status) =&student_gradeStatus($url,$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{$_} !~ /^correct/); - + $graded = 1 if ($status{$_} =~ /^ungraded/); + $incorrect = 1 if ($status{$_} =~ /^incorrect/); + my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; @@ -708,20 +714,25 @@ LISTJAVASCRIPT $status{'resource.'.$partid.'.submitted_by'}.'" />'; } } + next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'incorrect' || $submitonly eq 'graded')); - next if (!$graded && ($submitonly eq 'graded' || - $submitonly eq 'incorrect')); + next if (!$graded && ($submitonly eq 'graded')); + next if (!$incorrect && $submitonly eq 'incorrect'); } $ctr++; + my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()]; + if ( $perm{'vgr'} eq 'F' ) { $gradeTable.='<tr bgcolor="#ffffe6">' if ($ctr%2 ==1); $gradeTable.='<td align="right">'.$ctr.' </td>'. - '<td align="center"><input type=checkbox name="stuinfo" value="'. - $student.':'.$$fullname{$student}.' "></td>'."\n". - '<td>'.&nameUserString(undef,$$fullname{$student},$uname,$udom).'</td>'."\n"; + '<td align="center"><label><input type=checkbox name="stuinfo" value="'. + $student.':'.$$fullname{$student}.':::SECTION'.$section. + ') " /> </label></td>'."\n".'<td>'. + &nameUserString(undef,$$fullname{$student},$uname,$udom). + ' '.$section.'</td>'."\n"; if ($ENV{'form.showgrading'} eq 'yes' && $submitonly ne 'all') { foreach (sort keys(%status)) { @@ -743,7 +754,7 @@ LISTJAVASCRIPT $gradeTable.='</tr>'; } - $gradeTable.='</table></td></tr></table>'. + $gradeTable.='</table></td></tr></table>'."\n". '<input type="button" '. 'onClick="javascript:checkSelect(this.form.stuinfo);" '. 'value="Next->" /></form>'."\n"; @@ -768,6 +779,52 @@ LISTJAVASCRIPT } #---- Called from the listStudents routine + +sub check_script { + my ($form, $type)=@_; + my $chkallscript='<script type="text/javascript"> + function checkall() { + for (i=0; i<document.forms.'.$form.'.elements.length; i++) { + ele = document.forms.'.$form.'.elements[i]; + if (ele.name == "'.$type.'") { + document.forms.'.$form.'.elements[i].checked=true; + } + } + } + + function checksec() { + for (i=0; i<document.forms.'.$form.'.elements.length; i++) { + ele = document.forms.'.$form.'.elements[i]; + string = document.forms.'.$form.'.chksec.value; + if + (ele.value.indexOf(":::SECTION"+string)>0) { + document.forms.'.$form.'.elements[i].checked=true; + } + } + } + + + function uncheckall() { + for (i=0; i<document.forms.'.$form.'.elements.length; i++) { + ele = document.forms.'.$form.'.elements[i]; + if (ele.name == "'.$type.'") { + document.forms.'.$form.'.elements[i].checked=false; + } + } + } + +</script>'."\n"; + return $chkallscript; +} + +sub check_buttons { + my $buttons.='<input type="button" onclick="checkall()" value="Check All" />'; + $buttons.='<input type="button" onclick="uncheckall()" value="Uncheck All" /> '; + $buttons.='<input type="button" onclick="checksec()" value="Check Section/Group" />'; + $buttons.='<input type="text" size="5" name="chksec" /> '; + return $buttons; +} + # Displays the submissions for one student or a group of students sub processGroup { my ($request) = shift; @@ -2006,10 +2063,12 @@ sub processHandGrade { # my %record = &Apache::lonnet::restore($symb,$ENV{'request.course.id'},$udom,$uname); my %status=&student_gradeStatus($url,$symb,$udom,$uname,$partlist); my $submitted = 0; - my $graded = 1; + my $ungraded = 0; + my $incorrect = 0; foreach (keys(%status)) { $submitted = 1 if ($status{$_} ne 'nothing'); - $graded = 0 if ($status{$_} =~ /^correct/); + $ungraded = 1 if ($status{$_} =~ /^ungraded/); + $incorrect = 1 if ($status{$_} =~ /^incorrect/); my ($foo,$partid,$foo1) = split(/\./,$_); if ($status{'resource.'.$partid.'.submitted_by'} ne '') { $submitted = 0; @@ -2018,8 +2077,8 @@ sub processHandGrade { next if (!$submitted && ($submitonly eq 'yes' || $submitonly eq 'incorrect' || $submitonly eq 'graded')); - next if (!$graded && ($submitonly eq 'graded' || - $submitonly eq 'incorrect')); + next if (!$ungraded && ($submitonly eq 'graded')); + next if (!$incorrect && $submitonly eq 'incorrect'); } push @nextlist,$student if ($ctr < $ntstu); last if ($ctr == $ntstu); @@ -2054,6 +2113,7 @@ sub saveHandGrade { $ENV{'request.course.id'}); if (!&canmodify($usec)) { return('not_allowed'); } my %record = &Apache::lonnet::restore($symb,$ENV{'request.course.id'},$domain,$stuname); + my @parts_graded; my %newrecord = (); my ($pts,$wgt) = ('',''); foreach (split(/:/,$ENV{'form.partlist'.$newflg})) { @@ -2088,6 +2148,8 @@ sub saveHandGrade { if ($partial eq $record{'resource.'.$_.'.awarded'}) { #do not update score for part if not changed. next; + } else { + push @parts_graded, $_; } if ($record{'resource.'.$_.'.awarded'} ne $partial) { $newrecord{'resource.'.$_.'.awarded'} = $partial; @@ -2111,12 +2173,56 @@ sub saveHandGrade { } } if (scalar(keys(%newrecord)) > 0) { + &version_portfiles(\%record, \@parts_graded, $ENV{'request.course.id'}, $symb, $domain, $stuname); &Apache::lonnet::cstore(\%newrecord,$symb, $ENV{'request.course.id'},$domain,$stuname); } return '',$pts,$wgt; } +# ----------- Handles creating versions for portfolio files as answers +sub version_portfiles { + my ($record, $parts_graded, $courseid, $symb, $domain, $stuname) = @_; + my $parts = join '|', @$parts_graded; + my $portfolio_root = &Apache::loncommon::propath($domain, + $stuname). + '/userfiles/portfolio'; + foreach my $key (keys %$record) { + if ($key =~ /^resource\.($parts)\./ && $key =~ /\.portfiles$/) { + my @portfiles = split /,/,$$record{$key}; + foreach my $file (@portfiles) { + my ($directory,$answer_file) =( $file =~ /^(.*?)([^\/]*$)/); + my $version = 0; + my @answer_file_parts = split /\./, $answer_file; + my @dir_list = &Apache::lonnet::dirlist($directory,$domain,$stuname,$portfolio_root); + my @file_names; + my @file_name_parts; + foreach my $row (@dir_list) { + @file_names = split (/\&/,$row,2); + @file_name_parts = split (/\./, $file_names[0]); + # ($file_name_parts[scalar @file_name_parts] eq $answer_file_parts[scalar @answer_file_parts]) + if (($file_name_parts[0] eq $answer_file_parts[0]) && + ($file_name_parts[-1] eq $answer_file_parts[-1])) { + # gets here if filename and extension match, regardless of version + if (scalar @file_name_parts == 3) { # a versioned file is found + # so save it for later + if ($file_name_parts[1] > $version) {$version = $file_name_parts[1]}; + } + } + } + $version += 1; + &Apache::lonnet::logthis('answer file is '.$answer_file. + ' becomes '.$answer_file_parts[0].'.'.$version.'.'.$answer_file_parts[(scalar @answer_file_parts)-1]); + &Apache::lonnet::logthis('from dir list is '.$file_names[0].' has '.@file_name_parts.' parts'); + } + &Apache::lonnet::logthis('found key portfiles '.$key); + &Apache::lonnet::logthis('found value portfiles '.$$record{$key}); + } + } + + +} + #-------------------------------------------------------------------------------------- # #-------------------------- Next few routines handles grading by section or whole class @@ -2651,24 +2757,26 @@ sub split_part_type { # #--- Javascript to handle csv upload sub csvupload_javascript_reverse_associate { + my $error1=&mt('You need to specify the username or ID'); + my $error2=&mt('You need to specify at least one grading field'); return(<<ENDPICK); function verify(vf) { var foundsomething=0; var founduname=0; - var founddomain=0; + var foundID=0; for (i=0;i<=vf.nfields.value;i++) { tw=eval('vf.f'+i+'.selectedIndex'); - if (i==0 && tw!=0) { founduname=1; } - if (i==1 && tw!=0) { founddomain=1; } - if (i!=0 && i!=1 && tw!=0) { foundsomething=1; } - } - if (founduname==0 || founddomain==0) { - alert('You need to specify at both the username and domain'); - return; + if (i==0 && tw!=0) { foundID=1; } + if (i==1 && tw!=0) { founduname=1; } + if (i!=0 && i!=1 && i!=2 && tw!=0) { foundsomething=1; } + } + if (founduname==0 && foundID==0) { + alert('$error1'); + return; } if (foundsomething==0) { - alert('You need to specify at least one grading field'); - return; + alert('$error2'); + return; } vf.submit(); } @@ -2689,24 +2797,26 @@ ENDPICK } sub csvupload_javascript_forward_associate { + my $error1=&mt('You need to specify the username or ID'); + my $error2=&mt('You need to specify at least one grading field'); return(<<ENDPICK); function verify(vf) { var foundsomething=0; var founduname=0; - var founddomain=0; + var foundID=0; for (i=0;i<=vf.nfields.value;i++) { tw=eval('vf.f'+i+'.selectedIndex'); - if (tw==1) { founduname=1; } - if (tw==2) { founddomain=1; } - if (tw>2) { foundsomething=1; } - } - if (founduname==0 || founddomain==0) { - alert('You need to specify at both the username and domain'); - return; + if (tw==1) { foundID=1; } + if (tw==2) { founduname=1; } + if (tw>3) { foundsomething=1; } + } + if (founduname==0 && foundID==0) { + alert('$error1'); + return; } if (foundsomething==0) { - alert('You need to specify at least one grading field'); - return; + alert('$error2'); + return; } vf.submit(); } @@ -2733,7 +2843,8 @@ sub csvuploadmap_header { } my ($result) = &showResourceInfo($url,$ENV{'form.probTitle'}); - + my $checked=(($ENV{'form.noFirstLine'})?' checked="checked"':''); + my $ignore=&mt('Ignore First Line'); $request->print(<<ENDPICK); <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> <h3><font color="#339933">Uploading Class Grades</font></h3> @@ -2744,6 +2855,7 @@ Total number of records found in file: $ Enter as many fields as you can. The system will inform you and bring you back to this page if the data selected is insufficient to run your class.<hr /> <input type="button" value="Reverse Association" onClick="javascript:this.form.associate.value='Reverse Association';submit(this.form);" /> +<label><input type="checkbox" name="noFirstLine" $checked />$ignore</label> <input type="hidden" name="associate" value="" /> <input type="hidden" name="phase" value="three" /> <input type="hidden" name="datatoken" value="$datatoken" /> @@ -2755,7 +2867,7 @@ to this page if the data selected is ins <input type="hidden" name="url" value="$url" /> <input type="hidden" name="saveState" value="$ENV{'form.saveState'}" /> <input type="hidden" name="probTitle" value="$ENV{'form.probTitle'}" /> -<input type="hidden" name="command" value="csvuploadassign" /> +<input type="hidden" name="command" value="csvuploadoptions" /> <hr /> <script type="text/javascript" language="Javascript"> $javascript @@ -2768,13 +2880,18 @@ ENDPICK sub csvupload_fields { my ($url,$symb) = @_; my (@parts) = &getpartlist($url,$symb); - my @fields=(['username','Student Username'],['domain','Student Domain']); + my @fields=(['ID','Student ID'], + ['username','Student Username'], + ['domain','Student Domain']); foreach my $part (sort(@parts)) { my @datum; my $display=&Apache::lonnet::metadata($url,$part.'.display'); my $name=$part; if (!$display) { $display = $name; } @datum=($name,$display); + if ($name=~/^stores_(.*)_awarded/) { + push(@fields,['stores_'.$1.'_points',"Points [Part: $1]"]); + } push(@fields,\@datum); } return (@fields); @@ -2815,6 +2932,7 @@ CSVFORMJS '.</b></td></tr>'."\n"; $result.='<tr bgcolor=#ffffe6><td>'."\n"; my $upfile_select=&Apache::loncommon::upfile_select_html(); + my $ignore=&mt('Ignore First Line'); $result.=<<ENDUPFORM; <form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> <input type="hidden" name="symb" value="$symb" /> @@ -2824,7 +2942,7 @@ CSVFORMJS <input type="hidden" name="saveState" value="$ENV{'form.saveState'}" /> $upfile_select <br /><input type="button" onClick="javascript:checkUpload(this.form);" value="Upload Scores" /> - +<label><input type="checkbox" name="noFirstLine" />$ignore</lable> </form> ENDUPFORM $result.='</td></tr></table>'."\n"; @@ -2847,6 +2965,7 @@ sub csvuploadmap { &Apache::loncommon::load_tmp_file($request); } my @records=&Apache::loncommon::upfile_record_sep(); + if ($ENV{'form.noFirstLine'}) { shift(@records); } &csvuploadmap_header($request,$symb,$url,$datatoken,$#records+1); my ($i,$keyfields); if (@records) { @@ -2872,14 +2991,51 @@ sub csvuploadmap { return ''; } -sub csvuploadassign { +sub csvuploadoptions { my ($request)= @_; my ($symb,$url)=&get_symb_and_url($request); - if (!$symb) {return '';} - &Apache::loncommon::load_tmp_file($request); - my @gradedata = &Apache::loncommon::upfile_record_sep(); + my $checked=(($ENV{'form.noFirstLine'})?'1':'0'); + my $ignore=&mt('Ignore First Line'); + $request->print(<<ENDPICK); +<form method="post" enctype="multipart/form-data" action="/adm/grades" name="gradesupload"> +<h3><font color="#339933">Uploading Class Grade Options</font></h3> +<input type="hidden" name="command" value="csvuploadassign" /> +<input type="submit" value="Assign Grades" /><br /> +<p> +<label> + <input type="checkbox" name="show_full_results" /> + Show a table of all changes +</label> +</p> +<p> +<label> + <input type="checkbox" name="overwite_scores" checked="checked" /> + Overwrite any existing score +</label> +</p> +ENDPICK + my %fields=&get_fields(); + if (!defined($fields{'domain'})) { + my $domform = &Apache::loncommon::select_dom_form($ENV{'request.role.domain'},'default_domain'); + $request->print("\n<p> Users are in domain: ".$domform."</p>\n"); + } + foreach my $key (sort(keys(%ENV))) { + if ($key !~ /^form\.(.*)$/) { next; } + my $cleankey=$1; + if ($cleankey eq 'command') { next; } + $request->print('<input type="hidden" name="'.$cleankey. + '" value="'.$ENV{$key}.'" />'."\n"); + } + # FIXME do a check for any duplicated user ids... + # FIXME do a check for any invalid user ids?... + $request->print("<hr /></form>\n"); + $request->print(&show_grading_menu_form($symb,$url)); + return ''; +} + +sub get_fields { + my %fields; my @keyfields = split(/\,/,$ENV{'form.keyfields'}); - my %fields=(); for (my $i=0; $i<=$ENV{'form.nfields'}; $i++) { if ($ENV{'form.upfile_associate'} eq 'reverse') { if ($ENV{'form.f'.$i} ne 'none') { @@ -2891,6 +3047,17 @@ sub csvuploadassign { } } } + return %fields; +} + +sub csvuploadassign { + my ($request)= @_; + my ($symb,$url)=&get_symb_and_url($request); + if (!$symb) {return '';} + &Apache::loncommon::load_tmp_file($request); + my @gradedata = &Apache::loncommon::upfile_record_sep(); + if ($ENV{'form.noFirstLine'}) { shift(@gradedata); } + my %fields=&get_fields(); $request->print('<h3>Assigning Grades</h3>'); my $courseid=$ENV{'request.course.id'}; my ($classlist) = &getclasslist('all',0); @@ -2899,12 +3066,29 @@ sub csvuploadassign { my $countdone=0; foreach my $grade (@gradedata) { my %entries=&Apache::loncommon::record_sep($grade); + my $domain; + if ($entries{$fields{'domain'}}) { + $domain=$entries{$fields{'domain'}}; + } else { + $domain=$ENV{'form.default_domain'}; + } + $domain=~s/\s//g; my $username=$entries{$fields{'username'}}; $username=~s/\s//g; - my $domain=$entries{$fields{'domain'}}; - $domain=~s/\s//g; + if (!$username) { + my $id=$entries{$fields{'ID'}}; + $id=~s/\s//g; + my %ids=&Apache::lonnet::idget($domain,$id); + $username=$ids{$id}; + } if (!exists($$classlist{"$username:$domain"})) { - push(@skipped,"$username:$domain"); + my $id=$entries{$fields{'ID'}}; + $id=~s/\s//g; + if ($id) { + push(@skipped,"$id:$domain"); + } else { + push(@skipped,"$username:$domain"); + } next; } my $usec=$classlist->{"$username:$domain"}[5]; @@ -2912,16 +3096,34 @@ sub csvuploadassign { push(@notallowed,"$username:$domain"); next; } + my %points; my %grades; foreach my $dest (keys(%fields)) { - if ($dest eq 'username' || $dest eq 'domain') { next; } - if ($entries{$fields{$dest}} eq '') { next; } - my $store_key=$dest; - $store_key=~s/^stores/resource/; - $store_key=~s/_/\./g; - $grades{$store_key}=$entries{$fields{$dest}}; + if ($dest eq 'ID' || $dest eq 'username' || + $dest eq 'domain') { next; } + if ($entries{$fields{$dest}} =~ /^\s*$/) { next; } + if ($dest=~/stores_(.*)_points/) { + my $part=$1; + my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight', + $symb,$domain,$username); + $entries{$fields{$dest}}=~s/\s//g; + my $pcr=$entries{$fields{$dest}} / $wgt; + my $award='correct_by_override'; + $grades{"resource.$part.awarded"}=$pcr; + $grades{"resource.$part.solved"}=$award; + $points{$part}=1; + } else { + if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} } + if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} } + my $store_key=$dest; + $store_key=~s/^stores/resource/; + $store_key=~s/_/\./g; + $grades{$store_key}=$entries{$fields{$dest}}; + } } + if (! %grades) { push(@skipped,"$username:$domain no data to store"); } $grades{"resource.regrader"}="$ENV{'user.name'}:$ENV{'user.domain'}"; +# &Apache::lonnet::logthis(" storing ".(join('-',%grades))); &Apache::lonnet::cstore(\%grades,$symb,$ENV{'request.course.id'}, $domain,$username); $request->print('.'); @@ -5108,9 +5310,9 @@ sub handler { $request->print(&csvupload($request)); } elsif ($command eq 'csvuploadmap' && $perm{'mgr'} ) { $request->print(&csvuploadmap($request)); - } elsif ($command eq 'csvuploadassign' && $perm{'mgr'}) { + } elsif ($command eq 'csvuploadoptions' && $perm{'mgr'}) { if ($ENV{'form.associate'} ne 'Reverse Association') { - $request->print(&csvuploadassign($request)); + $request->print(&csvuploadoptions($request)); } else { if ( $ENV{'form.upfile_associate'} ne 'reverse' ) { $ENV{'form.upfile_associate'} = 'reverse'; @@ -5119,6 +5321,8 @@ sub handler { } $request->print(&csvuploadmap($request)); } + } elsif ($command eq 'csvuploadassign' && $perm{'mgr'} ) { + $request->print(&csvuploadassign($request)); } elsif ($command eq 'scantron_selectphase' && $perm{'mgr'}) { $request->print(&scantron_selectphase($request)); } elsif ($command eq 'scantron_warning' && $perm{'mgr'}) {