--- loncom/homework/grades.pm 2023/09/07 17:38:56 1.596.2.12.2.60.2.5 +++ loncom/homework/grades.pm 2024/12/03 23:34:10 1.796 @@ -1,7 +1,7 @@ # The LearningOnline Network with CAPA # The LON-CAPA Grading handler # -# $Id: grades.pm,v 1.596.2.12.2.60.2.5 2023/09/07 17:38:56 raeburn Exp $ +# $Id: grades.pm,v 1.796 2024/12/03 23:34:10 raeburn Exp $ # # Copyright Michigan State University Board of Trustees # @@ -44,12 +44,15 @@ use Apache::Constants qw(:common :http); use Apache::lonlocal; use Apache::lonenc; use Apache::lonstathelpers; +use Apache::lonquickgrades; use Apache::bridgetask(); use Apache::lontexconvert(); +use Apache::loncourserespicker; use String::Similarity; use HTML::Parser(); use File::MMagic; use LONCAPA; +use LONCAPA::ltiutils(); use POSIX qw(floor); @@ -146,7 +149,6 @@ sub nameUserString { } #--- Get the partlist and the response type for a given problem. --- -#--- Indicate if a response type is coded handgraded or not. --- #--- Count responseIDs, essayresponse items, and dropbox items --- #--- Sets response_error pointer to "1" if navmaps object broken --- sub response_type { @@ -636,7 +638,7 @@ COMMONJSFUNCTIONS #--- Dumps the class list with usernames,list of sections, #--- section, ids and fullnames for each user. sub getclasslist { - my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus) = @_; + my ($getsec,$filterbyaccstatus,$getgroup,$symb,$submitonly,$filterbysubmstatus,$filterbypbid,$possibles) = @_; my @getsec; my @getgroup; my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status')); @@ -664,12 +666,16 @@ sub getclasslist { # my %sections; my %fullnames; + my %passback; my ($cdom,$cnum,$partlist); if (($filterbysubmstatus) && ($submitonly ne 'all') && ($symb ne '')) { $cdom = $env{"course.$env{'request.course.id'}.domain"}; $cnum = $env{"course.$env{'request.course.id'}.num"}; my $res_error; ($partlist) = &response_type($symb,\$res_error); + } elsif ($filterbypbid) { + $cdom = $env{"course.$env{'request.course.id'}.domain"}; + $cnum = $env{"course.$env{'request.course.id'}.num"}; } foreach my $student (keys(%$classlist)) { my $end = @@ -756,6 +762,27 @@ sub getclasslist { } } } + if ($filterbypbid) { + if (ref($possibles) eq 'HASH') { + unless (exists($possibles->{$student})) { + delete($classlist->{$student}); + next; + } + } + my $udom = + $classlist->{$student}->[&Apache::loncoursedata::CL_SDOM()]; + my $uname = + $classlist->{$student}->[&Apache::loncoursedata::CL_SNAME()]; + if (($udom ne '') && ($uname ne '')) { + my %pbinfo = &Apache::lonnet::get('nohist_'.$cdom.'_'.$cnum.'_linkprot_pb',[$filterbypbid],$udom,$uname); + if (ref($pbinfo{$filterbypbid}) eq 'ARRAY') { + $passback{$student} = $pbinfo{$filterbypbid} + } else { + delete($classlist->{$student}); + next; + } + } + } $section = ($section ne '' ? $section : 'none'); if (&canview($section)) { if (!@getsec || grep(/^\Q$section\E$/,@getsec)) { @@ -771,7 +798,7 @@ sub getclasslist { } } my @sections = sort(keys(%sections)); - return ($classlist,\@sections,\%fullnames); + return ($classlist,\@sections,\%fullnames,\%passback); } sub canmodify { @@ -1034,6 +1061,611 @@ sub verifyreceipt { return $string; } +sub initialpassback { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my %passback = &Apache::lonnet::dump('nohist_linkprot_passback',$cdom,$cnum); + my $readonly; + unless ($perm{'mgr'}) { + $readonly = 1; + } + my $formname = 'initialpassback'; + my $navmap = Apache::lonnavmaps::navmap->new(); + my $output; + if (!defined($navmap)) { + if ($crstype eq 'Community') { + $output = &mt('Unable to retrieve information about community contents'); + } else { + $output = &mt('Unable to retrieve information about course contents'); + } + return '
'.$output.'
'; + } + return &Apache::loncourserespicker::create_picker($navmap,'passback',$formname,$crstype,undef, + undef,undef,undef,undef,undef,undef, + \%passback,$readonly); +} + +sub passback_filters { + my ($request,$symb) = @_; + my $cdom = $env{"course.$env{'request.course.id'}.domain"}; + my $cnum = $env{"course.$env{'request.course.id'}.num"}; + my $crstype = &Apache::loncommon::course_type(); + my ($launcher,$appname,$setter,$linkuri,$linkprotector,$scope,$chosen); + if ($env{'form.passback'} ne '') { + $chosen = &unescape($env{'form.passback'}); + ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + ($launcher,$appname,$setter) = &get_passback_launcher($cdom,$cnum,$chosen); + } + my $result; + if ($launcher ne '') { + $result = &launcher_info_box($launcher,$appname,$setter,$linkuri,$scope). + '
'.&mt('Set criteria to use to list students for possible passback of scores, then push Next [_1]',
+ '→').
+ '
'.&mt('Scores sent to launcher CMS').'
'.$outcome.'
'); + } else { + $request->print(''.&mt('No scores sent to launcher CMS').'
'); + } + if (keys(%tosend)) { + $request->print(''.&mt('No scores sent for following')); + my ($zeros,$nopbcreds,$noconfirm,$noscore); + 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))) { + next unless ($tosend{$student}); + my ($uname,$udom) = split(/:/,$student); + my $line = '
'.$error.'
'); + } + return; +} + +sub get_passback_launcher { + my ($cdom,$cnum,$chosen) = @_; + my ($linkuri,$linkprotector,$scope) = split("\0",$chosen); + my ($ltinum,$ltitype) = ($linkprotector =~ /^(\d+)(c|d)$/); + my ($appname,$setter); + if ($ltitype eq 'c') { + my %lti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider'); + if (ref($lti{$ltinum}) eq 'HASH') { + $appname = $lti{$ltinum}{'name'}; + if ($appname) { + $setter = ' (defined in course)'; + } + } + } elsif ($ltitype eq 'd') { + my %lti = &Apache::lonnet::get_domain_lti($cdom,'linkprot'); + if (ref($lti{$ltinum}) eq 'HASH') { + $appname = $lti{$ltinum}{'name'}; + if ($appname) { + $setter = ' (defined in domain)'; + } + } + } + if ($linkuri =~ m{^\Q/tiny/$cdom/\E(\w+)$}) { + my $key = $1; + my $tinyurl; + my ($result,$cached)=&Apache::lonnet::is_cached_new('tiny',$cdom."\0".$key); + if (defined($cached)) { + $tinyurl = $result; + } else { + my $configuname = &Apache::lonnet::get_domainconfiguser($cdom); + my %currtiny = &Apache::lonnet::get('tiny',[$key],$cdom,$configuname); + if ($currtiny{$key} ne '') { + $tinyurl = $currtiny{$key}; + &Apache::lonnet::do_cache_new('tiny',$cdom."\0".$key,$currtiny{$key},600); + } + } + if ($tinyurl) { + my ($crsnum,$launchsymb) = split(/\&/,$tinyurl); + if ($crsnum eq $cnum) { + my %passback = &Apache::lonnet::get('nohist_linkprot_passback',[$launchsymb],$cdom,$cnum); + if (ref($passback{$launchsymb}) eq 'HASH') { + if (exists($passback{$launchsymb}{$chosen})) { + return ($launchsymb,$appname,$setter) + } + } + } + } + } + return (); +} + +sub sections_and_groups { + my (@sections,@groups,$group_display); + @groups = &Apache::loncommon::get_env_multiple('form.group'); + if (grep(/^all$/,@groups)) { + @groups = ('all'); + $group_display = 'all'; + } elsif (grep(/^none$/,@groups)) { + @groups = ('none'); + $group_display = 'none'; + } elsif (@groups > 0) { + $group_display = join(', ',@groups); + } + if ($env{'request.course.sec'} ne '') { + @sections = ($env{'request.course.sec'}); + } else { + @sections = &Apache::loncommon::get_env_multiple('form.section'); + } + my $disabled = ' disabled="disabled"'; + if ($perm{'mgr'}) { + if (grep(/^all$/,@sections)) { + undef($disabled); + } else { + foreach my $sec (@sections) { + if (&canmodify($sec)) { + undef($disabled); + last; + } + } + } + } + if (grep(/^all$/,@sections)) { + @sections = ('all'); + } + return(\@sections,\@groups,$group_display,$disabled); +} + +sub launcher_info_box { + my ($launcher,$appname,$setter,$linkuri,$scope) = @_; + my $shownscope; + if ($scope eq 'res') { + $shownscope = &mt('Resource'); + } elsif ($scope eq 'map') { + $shownscope = &mt('Folder'); + } elsif ($scope eq 'rec') { + $shownscope = &mt('Folder + sub-folders'); + } + return ''. + &Apache::lonhtmlcommon::start_pick_box(). + &Apache::lonhtmlcommon::row_title(&mt('Launch Item Title')). + &Apache::lonnet::gettitle($launcher); + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Deep-link')). + $linkuri. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Launcher')). + $appname.' '.$setter. + &Apache::lonhtmlcommon::row_closure(). + &Apache::lonhtmlcommon::row_title(&mt('Score Type')). + $shownscope. + &Apache::lonhtmlcommon::row_closure(1). + &Apache::lonhtmlcommon::end_pick_box().'
'."\n"; +} + #--- 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 @@ -1065,34 +1697,8 @@ sub listStudents { } } - my %js_lt = &Apache::lonlocal::texthash ( - '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.', - ); - &js_escape(\%js_lt); + $request->print(&checkselect_js()); $request->print(&Apache::lonhtmlcommon::scripttag(<'.&mt('No grading privileges').'
'); - return; + $request->print(''.&mt('No grading privileges').'
'); + return; } else { $request->print(''."\n"); } @@ -2600,7 +3255,7 @@ sub submission { $msgfor =~ s/\'/\\'/g; #' stupid emacs - no! javascript $result.=''."\n". ''."\n". - ' '. &mt('Compose message to student'.(scalar(@$col_fullnames) >= 1 ? 's' : '')).')'. @@ -2608,7 +3263,7 @@ sub submission { '/mailbkgrd.gif" width="14" height="10" alt="" name="mailicon'.$counter.'" />'."\n". ''.&mt('[_1]Message:[_2] No more students for this section or class.','','').'
'."\n"; + my $the_end.=''.&mt('[_1]Message:[_2] No more students for this section or class.','','').'
'."\n"; $request->print($the_end); } return ''; @@ -3673,19 +4334,19 @@ sub handback_files { my $part_resp = join('_',@{ $part_response_id }); if (($env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'} =~ /^\d+$/) & ($new_part eq $part_id)) { for (my $counter=1; $counter<=$env{'form.'.$newflg.'_'.$part_resp.'_countreturndoc'}; $counter++) { - # if multiple files are uploaded names will be 'returndoc2','returndoc3' - if ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter}) { + # if multiple files are uploaded names will be 'returndoc2','returndoc3' + if ($env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter}) { my $fname=$env{'form.'.$newflg.'_'.$part_resp.'_returndoc'.$counter.'.filename'}; my ($directory,$answer_file) = ($env{'form.'.$newflg.'_'.$part_resp.'_origdoc'.$counter} =~ /^(.*?)([^\/]*)$/); my ($answer_name,$answer_ver,$answer_ext) = - &file_name_version_ext($answer_file); + &Apache::lonnet::file_name_version_ext($answer_file); my ($portfolio_path) = ($directory =~ /^.+$stuname\/portfolio(.*)/); my $getpropath = 1; my ($dir_list,$listerror) = &Apache::lonnet::dirlist($portfolio_root.$portfolio_path, $domain,$stuname,$getpropath); - my $version = &get_next_version($answer_name,$answer_ext,$dir_list); + my $version = &Apache::lonnet::get_next_version($answer_name,$answer_ext,$dir_list); # fix filename my ($save_file_name) = (($directory.$answer_name.".$version.".$answer_ext) =~ /^.+\/${stuname}\/(.*)/); my $result=&Apache::lonnet::finishuserfileupload($stuname,$domain, @@ -3703,8 +4364,7 @@ sub handback_files { $$newrecord{"resource.$new_part.$resp_id.handback"}.=','; } $$newrecord{"resource.$new_part.$resp_id.handback"} .= $save_file_name; - $file_msg.=''.$save_file_name."".&mt('Users are in domain: [_1]',$domform)."
\n"); + $request->print("\n".&mt('Users are in domain: [_1]',$domform)."
\n"); } foreach my $key (sort(keys(%env))) { if ($key !~ /^form\.(.*)$/) { next; } @@ -5114,11 +5698,10 @@ sub csvuploadassign { if (!$symb) {return '';} my $error_msg = ''; my $datatoken = &Apache::loncommon::valid_datatoken($env{'form.datatoken'}); - if ($datatoken ne '') { + if ($datatoken ne '') { &Apache::loncommon::load_tmp_file($request,$datatoken); } my @gradedata = &Apache::loncommon::upfile_record_sep(); - if ($env{'form.noFirstLine'}) { shift(@gradedata); } my %fields=&get_fields(); my $courseid=$env{'request.course.id'}; my ($classlist) = &getclasslist('all',0); @@ -5140,13 +5723,45 @@ sub csvuploadassign { if (!$username) { my $id=$entries{$fields{'ID'}}; $id=~s/\s//g; - my %ids=&Apache::lonnet::idget($domain,$id); - $username=$ids{$id}; + if ($id ne '') { + my %ids=&Apache::lonnet::idget($domain,[$id]); + $username=$ids{$id}; + } else { + if ($entries{$fields{'clicker'}}) { + my $clicker = $entries{$fields{'clicker'}}; + $clicker=~s/\s//g; + if ($clicker ne '') { + my %clickers = &Apache::lonnet::idget($domain,[$clicker],'clickers'); + if ($clickers{$clicker} ne '') { + my $match = 0; + my @inclass; + foreach my $poss (split(/,/,$clickers{$clicker})) { + if (exists($$classlist{"$poss:$domain"})) { + $username = $poss; + push(@inclass,$poss); + $match ++; + + } + } + if ($match > 1) { + undef($username); + $request->print(''. + &mt('Score not saved for clicker: [_1] (matched multiple usernames: [_2])', + $clicker,join(', ',@inclass)).'
'); + } + } + } + } + } } if (!exists($$classlist{"$username:$domain"})) { my $id=$entries{$fields{'ID'}}; $id=~s/\s//g; - if ($id) { + my $clicker = $entries{$fields{'clicker'}}; + $clicker=~s/\s//g; + if ($clicker) { + push(@skipped,"$clicker:$domain"); + } elsif ($id) { push(@skipped,"$id:$domain"); } else { push(@skipped,"$username:$domain"); @@ -5174,7 +5789,7 @@ sub csvuploadassign { my $award=($pcr == 0) ? 'incorrect_by_override' : 'correct_by_override'; if ($pcr>1) { - push(@warnings,&mt("[_1]: point value larger than weight","$username:$domain")); + push(@warnings,&mt("[_1]: point value larger than weight","$username:$domain")); } $grades{"resource.$part.awarded"}=$pcr; $grades{"resource.$part.solved"}=$award; @@ -5210,13 +5825,13 @@ sub csvuploadassign { $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}, $domain,$username); - } else { + $countdone++; + } else { $request->print("". &mt("Failed to save data for student [_1]. Message when trying to save was: [_2]", "$username:$domain",$result)."
"); } $request->rflush(); - $countdone++; } } $request->print('