--- loncom/homework/grades.pm 2023/02/12 21:01:30 1.792
+++ 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.792 2023/02/12 21:01:30 raeburn Exp $
+# $Id: grades.pm,v 1.796 2024/12/03 23:34:10 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -47,10 +47,12 @@ 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);
@@ -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]',
+ '→').
+ '
';
+ }
+ $result .= ''."\n";
+ return $result;
+}
+
+sub names_for_passback {
+ 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,$ctr,$newcommand,$submittext);
+ if ($launcher ne '') {
+ $result = &launcher_info_box($launcher,$appname,$setter,$linkuri,$scope);
+ }
+ $ctr = 0;
+ my @statuses = &Apache::loncommon::get_env_multiple('form.Status');
+ my $stu_status = join(':',@statuses);
+ $result .= ''."\n";
+ return $result;
+}
+
+sub do_passback {
+ 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);
+ }
+ if ($launcher ne '') {
+ $request->print(&launcher_info_box($launcher,$appname,$setter,$linkuri,$scope));
+ }
+ my $error;
+ if ($perm{'mgr'}) {
+ if ($launcher ne '') {
+ my @poss_students = &Apache::loncommon::get_env_multiple('form.stuinfo');
+ if (@poss_students) {
+ my %possibles;
+ foreach my $item (@poss_students) {
+ my ($stuname,$studom) = split(/:/,$item,3);
+ $possibles{$stuname.':'.$studom} = 1;
+ }
+ my ($sections,$groups,$group_display,$disabled) = §ions_and_groups();
+ my ($classlist,undef,$fullname,$pbinfo) =
+ &getclasslist($sections,'1',$groups,'','','',$chosen,\%possibles);
+ if ((ref($classlist) eq 'HASH') && (ref($pbinfo) eq 'HASH')) {
+ my %passback = %{$pbinfo};
+ my (%tosend,%remotenotok,%scorenotok,%zeroposs,%nopbinfo);
+ foreach my $possible (keys(%possibles)) {
+ if ((exists($classlist->{$possible})) &&
+ (exists($passback{$possible})) && (ref($passback{$possible}) eq 'ARRAY')) {
+ $tosend{$possible} = 1;
+ }
+ }
+ if (keys(%tosend)) {
+ my ($lti_in_use,$crsdef);
+ my ($ltinum,$ltitype) = ($linkprotector =~ /^(\d+)(c|d)$/);
+ if ($ltitype eq 'c') {
+ my %crslti = &Apache::lonnet::get_course_lti($cnum,$cdom,'provider');
+ $lti_in_use = $crslti{$ltinum};
+ $crsdef = 1;
+ } else {
+ my %domlti = &Apache::lonnet::get_domain_lti($cdom,'linkprot');
+ $lti_in_use = $domlti{$ltinum};
+ }
+ if (ref($lti_in_use) eq 'HASH') {
+ my $msgformat = $lti_in_use->{'passbackformat'};
+ my $keynum = $lti_in_use->{'cipher'};
+ my $scoretype = 'decimal';
+ if ($lti_in_use->{'scoreformat'} =~ /^(decimal|ratio|percentage)$/) {
+ $scoretype = $1;
+ }
+ my $pbsymb = &Apache::loncommon::symb_from_tinyurl($linkuri,$cnum,$cdom);
+ my $pbmap;
+ if ($pbsymb =~ /\.(page|sequence)$/) {
+ $pbmap = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pbsymb))[2]);
+ } else {
+ $pbmap = &Apache::lonnet::deversion((&Apache::lonnet::decode_symb($pbsymb))[0]);
+ }
+ $pbmap = &Apache::lonnet::clutter($pbmap);
+ my $pbscope;
+ if ($scope eq 'res') {
+ $pbscope = 'resource';
+ } elsif ($scope eq 'map') {
+ $pbscope = 'nonrec';
+ } elsif ($scope eq 'rec') {
+ $pbscope = 'map';
+ }
+ my $sigmethod = 'HMAC-SHA1';
+ my $type = 'linkprot';
+ my $clientip = &Apache::lonnet::get_requestor_ip();
+ my $lonhost = $Apache::lonnet::perlvar{'lonHostID'};
+ my $ip = &Apache::lonnet::get_host_ip($lonhost);
+ my $numstudents = scalar(keys(%tosend));
+ my %prog_state = &Apache::lonhtmlcommon::Create_PrgWin($request,$numstudents);
+ my $outcome = &Apache::loncommon::start_data_table().
+ &Apache::loncommon::start_data_table_header_row();
+ my $loop = 0;
+ while ($loop < 2) {
+ $outcome .= ''.&mt('No.').' | '.
+ ''.&nameUserString('header').' '.&mt('Section/Group').' | '.
+ ''.&mt('Score').' | ';
+ $loop++;
+ }
+ $outcome .= &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))) {
+ next unless ($tosend{$student});
+ my ($uname,$udom) = split(/:/,$student);
+ &Apache::lonhtmlcommon::Increment_PrgWin($request,\%prog_state,'last student');
+ my ($uname,$udom) = split(/:/,$student);
+ my $uhome = &Apache::lonnet::homeserver($uname,$udom),
+ my $id = $passback{$student}[0],
+ my $url = $passback{$student}[1],
+ my ($total,$possible,$usec);
+ if (ref($classlist->{$student}) eq 'ARRAY') {
+ $usec = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION];
+ }
+ if ($pbscope eq 'resource') {
+ $total = 0;
+ $possible = 0;
+ my $navmap = Apache::lonnavmaps::navmap->new($uname,$udom);
+ if (ref($navmap)) {
+ my $res = $navmap->getBySymb($pbsymb);
+ if (ref($res)) {
+ my $partlist = $res->parts();
+ if (ref($partlist) eq 'ARRAY') {
+ my %record = &Apache::lonnet::restore($pbsymb,$env{'request.course.id'},$udom,$uname);
+ foreach my $part (@{$partlist}) {
+ next if ($record{"resource.$part.solved"} =~/^excused/);
+ my $weight = &Apache::lonnet::EXT("resource.$part.weight",$pbsymb,$udom,$uname,$usec);
+ $possible += $weight;
+ if (($record{'version'}) && (exists($record{"resource.$part.awarded"}))) {
+ my $awarded = $record{"resource.$part.awarded"};
+ if ($awarded) {
+ $total += $weight * $awarded;
+ }
+ }
+ }
+ }
+ }
+ }
+ } elsif (($pbscope eq 'map') || ($pbscope eq 'nonrec')) {
+ ($total,$possible) =
+ &Apache::lonhomework::get_lti_score($uname,$udom,$pbmap,$pbscope);
+ }
+ if (($id ne '') && ($url ne '') && ($possible)) {
+ my ($sent,$score,$code,$result) =
+ &LONCAPA::ltiutils::send_grade($cdom,$cnum,$crsdef,$type,$ltinum,$keynum,$id,
+ $url,$scoretype,$sigmethod,$msgformat,$total,$possible);
+ my $no_passback;
+ if ($sent) {
+ if ($code == 200) {
+ delete($tosend{$student});
+ my $namespace = $cdom.'_'.$cnum.'_lp_passback';
+ my $store = {
+ 'score' => $score,
+ 'ip' => $ip,
+ 'host' => $lonhost,
+ 'protector' => $linkprotector,
+ 'deeplink' => $linkuri,
+ 'scope' => $scope,
+ 'url' => $url,
+ 'id' => $id,
+ 'clientip' => $clientip,
+ 'whodoneit' => $env{'user.name'}.':'.$env{'user.domain'},
+ };
+ my $value='';
+ foreach my $key (keys(%{$store})) {
+ $value.=&escape($key).'='.&Apache::lonnet::freeze_escape($store->{$key}).'&';
+ }
+ $value=~s/\&$//;
+ &Apache::lonnet::courselog(&escape($linkuri).':'.$uname.':'.$udom.':EXPORT:'.$value);
+ &Apache::lonnet::cstore({'score' => $score},$chosen,$namespace,$udom,$uname,'',$ip,1);
+ $ctr++;
+ if ($ctr%2 ==1) {
+ $outcome .= &Apache::loncommon::start_data_table_row();
+ }
+ my $section = $classlist->{$student}->[&Apache::loncoursedata::CL_SECTION()];
+ my $group = $classlist->{$student}->[&Apache::loncoursedata::CL_GROUP()];
+ $outcome .= ''.$ctr.' | '.
+ ''.&nameUserString(undef,$$fullname{$student},$uname,$udom).
+ ' '.$section.($group ne '' ?'/'.$group:'').' | '.
+ ''.$score.' | '."\n";
+ if ($ctr%2 ==0) {
+ $outcome .= &Apache::loncommon::end_data_table_row()."\n";
+ }
+ } else {
+ $remotenotok{$student} = 1;
+ $no_passback = "Passback response for ".$linkprotector." was $code ($result)";
+ &Apache::lonnet::logthis($no_passback." for $uname:$udom in ${cdom}_${cnum}");
+ }
+ } else {
+ $scorenotok{$student} = 1;
+ $no_passback = "Passback of grades not sent for ".$linkprotector;
+ &Apache::lonnet::logthis($no_passback." for $uname:$udom in ${cdom}_${cnum}");
+ }
+ if ($no_passback) {
+ &Apache::lonnet::log($udom,$uname,$uhome,$no_passback." score: $score; total: $total; possible: $possible");
+ my $ltigrade = {
+ 'ltinum' => $ltinum,
+ 'lti' => $lti_in_use,
+ 'crsdef' => $crsdef,
+ 'cid' => $cdom.'_'.$cnum,
+ 'uname' => $uname,
+ 'udom' => $udom,
+ 'uhome' => $uhome,
+ 'pbid' => $id,
+ 'pburl' => $url,
+ 'pbtype' => $type,
+ 'pbscope' => $pbscope,
+ 'pbmap' => $pbmap,
+ 'pbsymb' => $pbsymb,
+ 'format' => $scoretype,
+ 'scope' => $scope,
+ 'clientip' => $clientip,
+ 'linkprot' => $linkprotector,
+ 'total' => $total,
+ 'possible' => $possible,
+ 'score' => $score,
+ };
+ &Apache::lonnet::put('linkprot_passback_pending',$ltigrade,$cdom,$cnum);
+ }
+ } else {
+ if (($id ne '') && ($url ne '')) {
+ $zeroposs{$student} = 1;
+ } else {
+ $nopbinfo{$student} = 1;
+ }
+ }
+ }
+ &Apache::lonhtmlcommon::Close_PrgWin($request,\%prog_state);
+ if ($ctr%2 ==1) {
+ $outcome .= &Apache::loncommon::end_data_table_row();
+ }
+ $outcome .= &Apache::loncommon::end_data_table();
+ if ($ctr) {
+ $request->print('
'.&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 = '
'.&nameUserString(undef,$$fullname{$student},$uname,$udom).''."\n";
+ if ($zeroposs{$student}) {
+ $zeros .= $line;
+ } elsif ($nopbinfo{$student}) {
+ $nopbcreds .= $line;
+ } elsif ($remotenotok{$student}) {
+ $noconfirm .= $line;
+ } elsif ($scorenotok{$student}) {
+ $noscore .= $line;
+ }
+ }
+ if ($zeros) {
+ $request->print('
'.&mt('Total points possible was 0').':'.
+ '
');
+ }
+ if ($nopbcreds) {
+ $request->print('
'.&mt('Missing unique identifier and/or passback location').':'.
+ '
');
+ }
+ if ($noconfirm) {
+ $request->print('
'.&mt('Score receipt not confirmed by receiving CMS').':'.
+ '
');
+ }
+ if ($noscore) {
+ $request->print('
'.&mt('Score computation or transmission failed').':'.
+ '
');
+ }
+ $request->print('');
+ }
+ } else {
+ $error = &mt('Settings for deep-link launch target unavailable, so no scores were sent');
+ }
+ } else {
+ $error = &mt('No available students for whom scores can be sent.');
+ }
+ } else {
+ $error = &mt('Classlist could not be retrieved so no scores were sent.');
+ }
+ } else {
+ $error = &mt('No students selected to receive scores so none were sent.');
+ }
+ } else {
+ if ($env{'form.passback'}) {
+ $error = &mt('Deep-link launch target was invalid so no scores were sent.');
+ } else {
+ $error = &mt('Deep-link launch target was missing so no scores were sent.');
+ }
+ }
+ } else {
+ $error = &mt('You do not have permission to manage grades, so no scores were sent');
+ }
+ if ($error) {
+ $request->print(''.$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(< 1) {
- for (var i=0; i 'Please select a student or group of students before pushing the Save Scores button.',
+ 'single' => 'Please select the student before pushing the Save Scores button.',
+ );
+ } else {
+ %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);
+ return &Apache::lonhtmlcommon::scripttag(< 1) {
+ for (var i=0; i'.$string->[0].'';
- } elsif ($is_tool) {
- $lastsubonly =
- ''
- .''.&mt('Date Grade Passed Back:').' '.$timestamp."
\n";
- } else {
- my ($shownsubmdate,$showngradedate);
- if ($lastsubmittime && $lastgradetime) {
- $shownsubmdate = &Apache::lonlocal::locallocaltime($lastsubmittime);
- if ($lastgradetime > $lastsubmittime) {
- $showngradedate = &Apache::lonlocal::locallocaltime($lastgradetime);
- }
- } else {
- $shownsubmdate = $timestamp;
- }
- $lastsubonly =
- ''
- .'
'.&mt('Date Submitted:').' '.$shownsubmdate."\n";
- if ($showngradedate) {
- $lastsubonly .= '
'.&mt('Date Graded:').' '.$showngradedate."\n";
- }
-
- my %seenparts;
- my @part_response_id = &flatten_responseType($responseType);
- foreach my $part (@part_response_id) {
- my ($partid,$respid) = @{ $part };
- my $display_part=&get_display_part($partid,$symb);
- if ($env{"form.$uname:$udom:$partid:submitted_by"}) {
- if (exists($seenparts{$partid})) { next; }
- $seenparts{$partid}=1;
- $request->print(
- '
'.&mt('Part: [_1]',$display_part).''.
- '
'.&mt('Collaborative submission by: [_1]',
- ''.
- $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'').
- '
');
- next;
- }
- my $responsetype = $responseType->{$partid}->{$respid};
- if (!exists($record{"resource.$partid.$respid.submission"})) {
- $lastsubonly.="\n".''.
- ''.&mt('Part: [_1]',$display_part).''.
- ' '.
- '('.&mt('Response ID: [_1]',$respid).')'.
- ' '.
- ''.&mt('Nothing submitted - no attempts.').'
';
- next;
- }
- foreach my $submission (@$string) {
- my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/);
- if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; }
- my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4);
- # Similarity check
- my $similar='';
- my ($type,$trial,$rndseed);
- if ($hide eq 'rand') {
- $type = 'randomizetry';
- $trial = $record{"resource.$partid.tries"};
- $rndseed = $record{"resource.$partid.rndseed"};
- }
- if ($env{'form.checkPlag'}) {
- my ($oname,$odom,$ocrsid,$oessay,$osim)=
- &most_similar($uname,$udom,$symb,$subval);
- if ($osim) {
- $osim=int($osim*100.0);
- if ($hide eq 'anon') {
- $similar='
'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
'.
- &mt('As the current submission is for an anonymous survey, no other details are available.').'
';
- } else {
- $similar='
';
- if ($essayurl eq 'lib/templates/simpleproblem.problem') {
- $similar .= ''.
- &mt('Essay is [_1]% similar to an essay by [_2]',
- $osim,
- &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')').
- '
';
- } else {
- my %old_course_desc;
- if ($ocrsid ne '') {
- if (ref($coursedesc_by_cid{$ocrsid}) eq 'HASH') {
- %old_course_desc = %{$coursedesc_by_cid{$ocrsid}};
- } else {
- my $args;
- if ($ocrsid ne $env{'request.course.id'}) {
- $args = {'one_time' => 1};
- }
- %old_course_desc =
- &Apache::lonnet::coursedescription($ocrsid,$args);
- $coursedesc_by_cid{$ocrsid} = \%old_course_desc;
- }
- $similar .=
- ''.
- &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])',
- $osim,
- &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')',
- $old_course_desc{'description'},
- $old_course_desc{'num'},
- $old_course_desc{'domain'}).
- '
';
- } else {
- $similar .=
- ''.
- &mt('Essay is [_1]% similar to an essay by [_2] in an unknown course',
- $osim,
- &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')').
- '
';
- }
- }
- $similar .= ''.
- &keywords_highlight($oessay).
- '
';
- }
- }
- }
- my $order=&get_order($partid,$respid,$symb,$uname,$udom,
- undef,$type,$trial,$rndseed);
- if (($env{'form.lastSub'} eq 'lastonly') ||
- ($env{'form.lastSub'} eq 'datesub') ||
- ($env{'form.lastSub'} =~ /^(last|all)$/)) {
- my $display_part=&get_display_part($partid,$symb);
- $lastsubonly.=''.
- '
'.&mt('Part: [_1]',$display_part).''.
- '
'.
- '('.&mt('Response ID: [_1]',$respid).')'.
- ' ';
- my $files=&get_submitted_files($udom,$uname,$partid,$respid,\%record);
- if (@$files) {
- if ($hide eq 'anon') {
- $lastsubonly.='
'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files}));
- } else {
- $lastsubonly.='
'.'
'.&mt('Submitted Files:').''
- .'
';
- if(@$files == 1) {
- $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!');
- } else {
- $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!');
- }
- $lastsubonly .= '';
- foreach my $file (@$files) {
- &Apache::lonnet::allowuploaded('/adm/grades',$file);
- $lastsubonly.='
'.$file.'';
- }
- }
- $lastsubonly.='
';
- }
- if ($hide eq 'anon') {
- $lastsubonly.='
'.&mt('Anonymous Survey').'';
- } else {
- $lastsubonly.='
'.&mt('Submitted Answer:').' ';
- if ($draft) {
- $lastsubonly.= '
'.&mt('Draft Copy').'';
- }
- $subval =
- &cleanRecord($subval,$responsetype,$symb,$partid,
- $respid,\%record,$order,undef,$uname,$udom,$type,$trial,$rndseed);
- if ($responsetype eq 'essay') {
- $subval =~ s{\n}{
}g;
- }
- $lastsubonly.=$subval."\n";
- }
- if ($similar) {$lastsubonly.="
$similar\n";}
- $lastsubonly.='
';
- }
- }
- }
- $lastsubonly.=' '."\n"; # End: LC_grade_submissions_body
- }
+ my ($lastsubonly,$partinfo) =
+ &show_last_submission($uname,$udom,$symb,$essayurl,$responseType,$env{'form.lastSub'},
+ $is_tool,$fullname,\%record,\%coursedesc_by_cid);
+ $request->print($partinfo);
$request->print($lastsubonly);
+
if ($env{'form.lastSub'} eq 'datesub') {
my ($parts,$handgrade,$responseType) = &response_type($symb,\$res_error);
$request->print(&displaySubByDates($symb,\%record,$parts,$responseType,$checkIcon,$uname,$udom));
@@ -2857,6 +3341,186 @@ sub submission {
return '';
}
+sub show_last_submission {
+ my ($uname,$udom,$symb,$essayurl,$responseType,$viewtype,$is_tool,$fullname,
+ $record,$coursedesc_by_cid) = @_;
+ my ($string,$timestamp,$lastgradetime,$lastsubmittime) =
+ &get_last_submission($record,$is_tool);
+
+ my ($lastsubonly,$partinfo);
+ if ($timestamp eq '') {
+ $lastsubonly.=''.$string->[0].'
';
+ } elsif ($is_tool) {
+ $lastsubonly =
+ ''
+ .''.&mt('Date Grade Passed Back:').' '.$timestamp."
\n";
+ } else {
+ my ($shownsubmdate,$showngradedate);
+ if ($lastsubmittime && $lastgradetime) {
+ $shownsubmdate = &Apache::lonlocal::locallocaltime($lastsubmittime);
+ if ($lastgradetime > $lastsubmittime) {
+ $showngradedate = &Apache::lonlocal::locallocaltime($lastgradetime);
+ }
+ } else {
+ $shownsubmdate = $timestamp;
+ }
+ $lastsubonly =
+ ''
+ .'
'.&mt('Date Submitted:').' '.$shownsubmdate."\n";
+ if ($showngradedate) {
+ $lastsubonly .= '
'.&mt('Date Graded:').' '.$showngradedate."\n";
+ }
+
+ my %seenparts;
+ my @part_response_id = &flatten_responseType($responseType);
+ foreach my $part (@part_response_id) {
+ my ($partid,$respid) = @{ $part };
+ my $display_part=&get_display_part($partid,$symb);
+ if ($env{"form.$uname:$udom:$partid:submitted_by"}) {
+ if (exists($seenparts{$partid})) { next; }
+ $seenparts{$partid}=1;
+ $partinfo .=
+ '
'.&mt('Part: [_1]',$display_part).''.
+ '
'.&mt('Collaborative submission by: [_1]',
+ ''.
+ $$fullname{$env{"form.$uname:$udom:$partid:submitted_by"}}.'').
+ '
';
+ next;
+ }
+ my $responsetype = $responseType->{$partid}->{$respid};
+ if (!exists($record->{"resource.$partid.$respid.submission"})) {
+ $lastsubonly.="\n".''.
+ ''.&mt('Part: [_1]',$display_part).''.
+ ' '.
+ '('.&mt('Response ID: [_1]',$respid).')'.
+ ' '.
+ ''.&mt('Nothing submitted - no attempts.').'
';
+ next;
+ }
+ foreach my $submission (@$string) {
+ my ($partid,$respid) = ($submission =~ /^resource\.([^\.]*)\.([^\.]*)\.submission/);
+ if (join('_',@{$part}) ne ($partid.'_'.$respid)) { next; }
+ my ($ressub,$hide,$draft,$subval) = split(/:/,$submission,4);
+ # Similarity check
+ my $similar='';
+ my ($type,$trial,$rndseed);
+ if ($hide eq 'rand') {
+ $type = 'randomizetry';
+ $trial = $record->{"resource.$partid.tries"};
+ $rndseed = $record->{"resource.$partid.rndseed"};
+ }
+ if ($env{'form.checkPlag'}) {
+ my ($oname,$odom,$ocrsid,$oessay,$osim)=
+ &most_similar($uname,$udom,$symb,$subval);
+ if ($osim) {
+ $osim=int($osim*100.0);
+ if ($hide eq 'anon') {
+ $similar='
'.&mt("Essay was found to be similar to another essay submitted for this assignment.").'
'.
+ &mt('As the current submission is for an anonymous survey, no other details are available.').'
';
+ } else {
+ $similar='
';
+ if ($essayurl eq 'lib/templates/simpleproblem.problem') {
+ $similar .= ''.
+ &mt('Essay is [_1]% similar to an essay by [_2]',
+ $osim,
+ &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')').
+ '
';
+ } else {
+ my %old_course_desc;
+ if ($ocrsid ne '') {
+ if (ref($coursedesc_by_cid->{$ocrsid}) eq 'HASH') {
+ %old_course_desc = %{$coursedesc_by_cid->{$ocrsid}};
+ } else {
+ my $args;
+ if ($ocrsid ne $env{'request.course.id'}) {
+ $args = {'one_time' => 1};
+ }
+ %old_course_desc =
+ &Apache::lonnet::coursedescription($ocrsid,$args);
+ $coursedesc_by_cid->{$ocrsid} = \%old_course_desc;
+ }
+ $similar .=
+ ''.
+ &mt('Essay is [_1]% similar to an essay by [_2] in course [_3] (course id [_4]:[_5])',
+ $osim,
+ &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')',
+ $old_course_desc{'description'},
+ $old_course_desc{'num'},
+ $old_course_desc{'domain'}).
+ '
';
+ } else {
+ $similar .=
+ ''.
+ &mt('Essay is [_1]% similar to an essay by [_2] in an unknown course',
+ $osim,
+ &Apache::loncommon::plainname($oname,$odom).' ('.$oname.':'.$odom.')').
+ '
';
+ }
+ }
+ $similar .= ''.
+ &keywords_highlight($oessay).
+ '
';
+ }
+ }
+ }
+ my $order=&get_order($partid,$respid,$symb,$uname,$udom,
+ undef,$type,$trial,$rndseed);
+ if (($viewtype eq 'lastonly') ||
+ ($viewtype eq 'datesub') ||
+ ($viewtype =~ /^(last|all)$/)) {
+ my $display_part=&get_display_part($partid,$symb);
+ $lastsubonly.=''.
+ '
'.&mt('Part: [_1]',$display_part).''.
+ '
'.
+ '('.&mt('Response ID: [_1]',$respid).')'.
+ ' ';
+ my $files=&get_submitted_files($udom,$uname,$partid,$respid,$record);
+ if (@$files) {
+ if ($hide eq 'anon') {
+ $lastsubonly.='
'.&mt('[quant,_1,file] uploaded to this anonymous survey',scalar(@{$files}));
+ } else {
+ $lastsubonly.='
'.'
'.&mt('Submitted Files:').''
+ .'
';
+ if(@$files == 1) {
+ $lastsubonly .= &mt('Like all files provided by users, this file may contain viruses!');
+ } else {
+ $lastsubonly .= &mt('Like all files provided by users, these files may contain viruses!');
+ }
+ $lastsubonly .= '';
+ foreach my $file (@$files) {
+ &Apache::lonnet::allowuploaded('/adm/grades',$file);
+ $lastsubonly.='
'.$file.'';
+ }
+ }
+ $lastsubonly.='
';
+ }
+ if ($hide eq 'anon') {
+ $lastsubonly.='
'.&mt('Anonymous Survey').'';
+ } else {
+ $lastsubonly.='
'.&mt('Submitted Answer:').' ';
+ if ($draft) {
+ $lastsubonly.= '
'.&mt('Draft Copy').'';
+ }
+ $subval =
+ &cleanRecord($subval,$responsetype,$symb,$partid,
+ $respid,$record,$order,undef,$uname,$udom,$type,$trial,$rndseed);
+ if ($responsetype eq 'essay') {
+ $subval =~ s{\n}{
}g;
+ }
+ $lastsubonly.=$subval."\n";
+ }
+ if ($similar) {$lastsubonly.="
$similar\n";}
+ $lastsubonly.='
';
+ }
+ }
+ }
+ $lastsubonly.=' '."\n"; # End: LC_grade_submissions_body
+ }
+ return ($lastsubonly,$partinfo);
+}
+
sub check_collaborators {
my ($symb,$uname,$udom,$record,$handgrade,$counter) = @_;
my ($result,@col_fullnames);
@@ -2953,9 +3617,14 @@ sub get_last_submission {
$prevsolved{$partid} = $solved{$partid};
}
}
- $timestamp =
- &Apache::lonlocal::locallocaltime($$returnhash{$version.':timestamp'});
}
+#
+# Timestamp is for last transaction for this resource, which does not
+# necessarily correspond to the time of last submission for problem (or part).
+#
+ if ($lasthash{'timestamp'} ne '') {
+ $timestamp = &Apache::lonlocal::locallocaltime($lasthash{'timestamp'});
+ }
my (%typeparts,%randombytry);
my $showsurv =
&Apache::lonnet::allowed('vas',$env{'request.course.id'});
@@ -3838,8 +4507,8 @@ sub version_portfiles {
$$record{$key} = join(',',@versioned_portfiles);
push(@returned_keys,$key);
}
- }
- return (@returned_keys);
+ }
+ return (@returned_keys);
}
#--------------------------------------------------------------------------------------
@@ -10545,7 +11214,8 @@ sub verify_scantron_grading {
sub href_symb_cmd {
my ($symb,$cmd)=@_;
- return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='.$cmd;
+ return '/adm/grades?symb='.&HTML::Entities::encode(&Apache::lonenc::check_encrypt($symb),'<>&"').'&command='.
+ &HTML::Entities::encode($cmd,'<>&"');
}
sub grading_menu {
@@ -10654,7 +11324,20 @@ sub grading_menu {
]
});
-
+ my $cdom = $env{"course.$env{'request.course.id'}.domain"};
+ my $cnum = $env{"course.$env{'request.course.id'}.num"};
+ my %passback = &Apache::lonnet::dump('nohist_linkprot_passback',$cdom,$cnum);
+ if (keys(%passback)) {
+ $fields{'command'} = 'initialpassback';
+ my $url6 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@{$menu[1]{items}},
+ { linktext => 'Passback of Scores',
+ url => $url6,
+ permission => $permissions{'either'},
+ icon => 'passback.png',
+ linktitle => 'Passback scores to launcher CMS for resources accessed via LTI-mediated deep-linking',
+ });
+ }
# Create the menu
my $Str;
$Str .= '');
}
+#----- display problem, answer, and submissions for a single student (no grading)
+
+sub view_as_user {
+ my ($symb,$vuname,$vudom,$hasperm) = @_;
+ my $plainname = &Apache::loncommon::plainname($vuname,$vudom,'lastname');
+ my $displayname = &nameUserString('',$plainname,$vuname,$vudom);
+ my $output = &Apache::loncommon::get_student_view($symb,$vuname,$vudom,
+ $env{'request.course.id'},
+ undef,{'disable_submit' => 1}).
+ "\n\n".
+ ''.
+ '
'.$displayname.'
'.
+ "\n".
+ &Apache::loncommon::track_student_link('View recent activity',
+ $vuname,$vudom,'check').' '.
+ "\n";
+ if (&Apache::lonnet::allowed('opa',$env{'request.course.id'}) ||
+ (($env{'request.course.sec'} ne '') &&
+ &Apache::lonnet::allowed('opa',$env{'request.course.id'}.'/'.$env{'request.course.sec'}))) {
+ $output .= &Apache::loncommon::pprmlink(&mt('Set/Change parameters'),
+ $vuname,$vudom,$symb,'check');
+ }
+ $output .= "\n";
+ my $companswer = &Apache::loncommon::get_student_answers($symb,$vuname,$vudom,
+ $env{'request.course.id'});
+ $companswer=~s|
||g;
+ $companswer=~s|name="submit"|name="would_have_been_submit"|g;
+ $output .= '
'.
+ '
'.&mt('Correct answer for[_1]',$displayname).'
'.
+ $companswer.
+ ''."\n";
+ my $is_tool = ($symb =~ /ext\.tool$/);
+ my ($essayurl,%coursedesc_by_cid);
+ (undef,undef,$essayurl) = &Apache::lonnet::decode_symb($symb);
+ my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$vudom,$vuname);
+ my $res_error;
+ my ($partlist,$handgrade,$responseType,$numresp,$numessay) =
+ &response_type($symb,\$res_error);
+ my $fullname;
+ my $collabinfo;
+ if ($numessay) {
+ unless ($hasperm) {
+ &init_perm();
+ }
+ ($collabinfo,$fullname)=
+ &check_collaborators($symb,$vuname,$vudom,\%record,$handgrade,0);
+ unless ($hasperm) {
+ &reset_perm();
+ }
+ }
+ my $checkIcon = '
';
+ my ($lastsubonly,$partinfo) =
+ &show_last_submission($vuname,$vudom,$symb,$essayurl,$responseType,'datesub',
+ '',$fullname,\%record,\%coursedesc_by_cid);
+ $output .= '
'.
+ '
'.&mt('Submissions').'
'."\n".$collabinfo."\n";
+ if (($numresp > $numessay) & !$is_tool) {
+ $output .='
'.
+ &mt('Part(s) graded correct by the computer is marked with a [_1] symbol.',$checkIcon).
+ "
\n";
+ }
+ $output .= $partinfo;
+ $output .= $lastsubonly;
+ $output .= &displaySubByDates($symb,\%record,$partlist,$responseType,$checkIcon,$vuname,$vudom);
+ $output .= '
'."\n";
+ return $output;
+}
+
sub handler {
my $request=$_[0];
&reset_caches();
@@ -11803,6 +12557,44 @@ sub handler {
undef,undef,undef,undef,undef,undef,undef,1);
$request->print('');
&submit_download_link($request,$symb);
+ } elsif ($command eq 'initialpassback') {
+ &startpage($request,$symb,[{href=>'', text=>'Choose Launcher'}],undef,1);
+ $request->print(&initialpassback($request,$symb));
+ } elsif ($command eq 'passback') {
+ &startpage($request,$symb,
+ [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'},
+ {href=>'', text=>'Types of User'}],undef,1);
+ $request->print(&passback_filters($request,$symb));
+ } elsif ($command eq 'passbacknames') {
+ my $chosen;
+ if ($env{'form.passback'} ne '') {
+ if ($env{'form.passback'} eq &unescape($env{'form.passback'})) {
+ $env{'form.passback'} = &escape($env{'form.passback'} );
+ }
+ $chosen = &HTML::Entities::encode($env{'form.passback'},'<>"&');
+ }
+ &startpage($request,$symb,
+ [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'},
+ {href=>&href_symb_cmd($symb,'passback').'&passback='.$chosen, text=>'Types of User'},
+ {href=>'', text=>'Select Users'}],undef,1);
+ $request->print(&names_for_passback($request,$symb));
+ } elsif ($command eq 'passbackscores') {
+ my ($chosen,$stu_status);
+ if ($env{'form.passback'} ne '') {
+ if ($env{'form.passback'} eq &unescape($env{'form.passback'})) {
+ $env{'form.passback'} = &escape($env{'form.passback'} );
+ }
+ $chosen = &HTML::Entities::encode($env{'form.passback'},'<>"&');
+ }
+ if ($env{'form.Status'}) {
+ $stu_status = &HTML::Entities::encode($env{'form.Status'});
+ }
+ &startpage($request,$symb,
+ [{href=>&href_symb_cmd($symb,'initialpassback'), text=>'Choose Launcher'},
+ {href=>&href_symb_cmd($symb,'passback').'&passback='.$chosen, text=>'Types of User'},
+ {href=>&href_symb_cmd($symb,'passbacknames').'&Status='.$stu_status.'&passback='.$chosen, text=>'Select Users'},
+ {href=>'', text=>'Execute Passback'}],undef,1);
+ $request->print(&do_passback($request,$symb));
} elsif ($command) {
&startpage($request,$symb,[{href=>'', text=>'Access denied'}]);
$request->print(''.&mt('Access Denied ([_1])',$command).'
');