');
+ }
+ if ($noconfirm) {
+ $request->print(' '.&mt('Score receipt not confirmed by receiving CMS').':'.
+ '
'.$noconfirm.'
');
+ }
+ if ($noscore) {
+ $request->print(' '.&mt('Score computation or transmission failed').':'.
+ '
'.$noscore.'
');
+ }
+ $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 '
'.$line);
@@ -4642,7 +5911,7 @@ ENDUPFORM
sub csvuploadmap {
- my ($request,$symb)= @_;
+ my ($request,$symb) = @_;
if (!$symb) {return '';}
my $datatoken;
@@ -4738,7 +6007,7 @@ sub get_fields {
}
sub csvuploadassign {
- my ($request,$symb)= @_;
+ my ($request,$symb) = @_;
if (!$symb) {return '';}
my $error_msg = '';
my $datatoken = &Apache::loncommon::valid_datatoken($env{'form.datatoken'});
@@ -4748,11 +6017,33 @@ sub csvuploadassign {
my @gradedata = &Apache::loncommon::upfile_record_sep();
my %fields=&get_fields();
my $courseid=$env{'request.course.id'};
+ my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
my ($classlist) = &getclasslist('all',0);
my @notallowed;
my @skipped;
my @warnings;
my $countdone=0;
+ my @parts;
+ my %needpb = &passbacks_for_symb($cdom,$cnum,$symb);
+ my $passback;
+ if (keys(%needpb)) {
+ $passback = 1;
+ my $navmap = Apache::lonnavmaps::navmap->new();
+ if (ref($navmap)) {
+ my $res = $navmap->getBySymb($symb);
+ if (ref($res)) {
+ my $partlist = $res->parts();
+ if (ref($partlist) eq 'ARRAY') {
+ @parts = sort(@{$partlist});
+ }
+ }
+ } else {
+ return &navmap_errormsg();
+ }
+ }
+ my (%skip_passback,%pbsave,%weights,%awardeds,%excuseds);
+
foreach my $grade (@gradedata) {
my %entries=&Apache::loncommon::record_sep($grade);
my $domain;
@@ -4827,9 +6118,14 @@ sub csvuploadassign {
my $part=$1;
my $wgt =&Apache::lonnet::EXT('resource.'.$part.'.weight',
$symb,$domain,$username);
+ $weights{$symb}{$part} = $wgt;
if ($wgt) {
$entries{$fields{$dest}}=~s/\s//g;
my $pcr=$entries{$fields{$dest}} / $wgt;
+ if ($passback) {
+ $awardeds{$symb}{$part} = $pcr;
+ $excuseds{$symb}{$part} = '';
+ }
my $award=($pcr == 0) ? 'incorrect_by_override'
: 'correct_by_override';
if ($pcr>1) {
@@ -4849,12 +6145,28 @@ sub csvuploadassign {
if ($dest=~/stores_(.*)_awarded/) { if ($points{$1}) {next;} }
if ($dest=~/stores_(.*)_solved/) { if ($points{$1}) {next;} }
my $store_key=$dest;
+ if ($passback) {
+ if ($store_key=~/stores_(.*)_(awarded|solved)/) {
+ my ($part,$key) = ($1,$2);
+ unless ((ref($weights{$symb}) eq 'HASH') && (exists($weights{$symb}{$part}))) {
+ $weights{$symb}{$part} = &Apache::lonnet::EXT('resource.'.$part.'.weight',
+ $symb,$domain,$username);
+ }
+ if ($key eq 'awarded') {
+ $awardeds{$symb}{$part} = $entries{$fields{$dest}};
+ } elsif ($key eq 'solved') {
+ if ($entries{$fields{$dest}} =~ /^excused/) {
+ $excuseds{$symb}{$part} = 1;
+ }
+ }
+ }
+ }
$store_key=~s/^stores/resource/;
$store_key=~s/_/\./g;
$grades{$store_key}=$entries{$fields{$dest}};
}
}
- if (! %grades) {
+ if (! %grades) {
push(@skipped,&mt("[_1]: no data to save","$username:$domain"));
} else {
$grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
@@ -4865,11 +6177,32 @@ sub csvuploadassign {
# Successfully stored
$request->print('.');
# Remove from grading queue
- &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,
- $env{'course.'.$env{'request.course.id'}.'.domain'},
- $env{'course.'.$env{'request.course.id'}.'.num'},
+ &Apache::bridgetask::remove_from_queue('gradingqueue',$symb,$cdom,$cnum,
$domain,$username);
$countdone++;
+ if ($passback) {
+ my @parts_in_upload;
+ if (ref($weights{$symb}) eq 'HASH') {
+ @parts_in_upload = sort(keys(%{$weights{$symb}}));
+ }
+ my @diffs = &Apache::loncommon::compare_arrays(\@parts_in_upload,\@parts);
+ if (@diffs > 0) {
+ my %record = &Apache::lonnet::restore($symb,$env{'request.course.id'},$domain,$username);
+ foreach my $part (@parts) {
+ next if (grep(/^\Q$part\E$/,@parts_in_upload));
+ $weights{$symb}{$part} = &Apache::lonnet::EXT('resource.'.$part.'.weight',
+ $symb,$domain,$username);
+ if ($record{"resource.$part.solved"} =~/^excused/) {
+ $excuseds{$symb}{$part} = 1;
+ } else {
+ $excuseds{$symb}{$part} = '';
+ }
+ $awardeds{$symb}{$part} = $record{"resource.$part.awarded"};
+ }
+ }
+ &process_passbacks('csvupload',[$symb],$cdom,$cnum,$domain,$username,\%weights,
+ \%awardeds,\%excuseds,\%needpb,\%skip_passback,\%pbsave);
+ }
} else {
$request->print("
".
&mt("Failed to save data for student [_1]. Message when trying to save was: [_2]",
@@ -5242,10 +6575,14 @@ sub displayPage {
}
$curRes = $iterator->next();
}
+ my $disabled;
+ unless (&canmodify($usec)) {
+ $disabled = ' disabled="disabled"';
+ }
$studentTable.=
''."\n".
- ''.
''."\n";
$request->print($studentTable);
@@ -5311,11 +6648,11 @@ sub displaySubByDates {
}
my @matchKey;
if ($isTask) {
- @matchKey = sort(grep /^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys);
+ @matchKey = sort(grep(/^resource\.\d+\.\Q$partid\E\.award$/,@versionKeys));
} elsif ($is_tool) {
- @matchKey = sort(grep /^resource\.\Q$partid\E\.awarded$/,@versionKeys);
+ @matchKey = sort(grep(/^resource\.\Q$partid\E\.awarded$/,@versionKeys));
} else {
- @matchKey = sort(grep /^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys);
+ @matchKey = sort(grep(/^resource\.\Q$partid\E\..*?\.submission$/,@versionKeys));
}
# next if ($$record{"$version:resource.$partid.solved"} eq '');
my $display_part=&get_display_part($partid,$symb);
@@ -5461,6 +6798,7 @@ sub updateGradeByPage {
$iterator->next(); # skip the first BEGIN_MAP
my $curRes = $iterator->next(); # for "current resource"
my ($depth,$question,$prob,$changeflag,$hideflag)= (1,1,1,0,0);
+ my (@updates,%weights,%excuseds,%awardeds,@symbs_in_map);
while ($depth > 0) {
if($curRes == $iterator->BEGIN_MAP) { $depth++; }
if($curRes == $iterator->END_MAP) { $depth--; }
@@ -5469,6 +6807,7 @@ sub updateGradeByPage {
my $parts = $curRes->parts();
my $title = $curRes->compTitle();
my $symbx = $curRes->symb();
+ push(@symbs_in_map,$symbx);
$studentTable.=
&Apache::loncommon::start_data_table_row().
'
'.$prob.
@@ -5481,18 +6820,37 @@ sub updateGradeByPage {
my @displayPts=();
my %aggregate = ();
my $aggregateflag = 0;
+ my %queueable;
if ($env{'form.HIDE'.$prob}) {
my %record = &Apache::lonnet::restore($symbx,$env{'request.course.id'},$udom,$uname);
my ($version,$parts) = split(/:/,$env{'form.HIDE'.$prob},2);
my $numchgs = &makehidden($version,$parts,\%record,$symbx,$udom,$uname,1);
+ if ($numchgs) {
+ push(@updates,$symbx);
+ }
$hideflag += $numchgs;
}
foreach my $partid (@{$parts}) {
my $newpts = $env{'form.GD_BOX'.$question.'_'.$partid};
my $oldpts = $env{'form.oldpts'.$question.'_'.$partid};
-
+ my @types = $curRes->responseType($partid);
+ if (grep(/^essay$/,@types)) {
+ $queueable{$partid} = 1;
+ } else {
+ my @ids = $curRes->responseIds($partid);
+ for (my $i=0; $i < scalar(@ids); $i++) {
+ my $hndgrd = &Apache::lonnet::EXT('resource.'.$partid.'_'.$ids[$i].
+ '.handgrade',$symb);
+ if (lc($hndgrd) eq 'yes') {
+ $queueable{$partid} = 1;
+ last;
+ }
+ }
+ }
my $wgt = $env{'form.WGT'.$question.'_'.$partid} != 0 ?
$env{'form.WGT'.$question.'_'.$partid} : 1;
+ $weights{$symbx}{$partid} = $wgt;
+ $excuseds{$symbx}{$partid} = '';
my $partial = $newpts/$wgt;
my $score;
if ($partial > 0) {
@@ -5504,6 +6862,7 @@ sub updateGradeByPage {
if ($dropMenu eq 'excused') {
$partial = '';
$score = 'excused';
+ $excuseds{$symbx}{$partid} = 1;
} elsif ($dropMenu eq 'reset status'
&& $env{'form.solved'.$question.'_'.$partid} ne '') { #update only if previous record exists
$newrecord{'resource.'.$partid.'.tries'} = 0;
@@ -5531,6 +6890,11 @@ sub updateGradeByPage {
(($score eq 'excused') ? 'excused' : $newpts).
' ';
$question++;
+ if (($newpts eq '') || ($partial eq '')) {
+ $awardeds{$symbx}{$partid} = 0;
+ } else {
+ $awardeds{$symbx}{$partid} = $partial;
+ }
next if ($dropMenu eq 'reset status' || ($newpts eq $oldpts && $score ne 'excused'));
$newrecord{'resource.'.$partid.'.awarded'} = $partial if $partial ne '';
@@ -5556,7 +6920,7 @@ sub updateGradeByPage {
$env{'request.course.id'},
$udom,$uname);
&check_and_remove_from_queue($parts,\%record,undef,$symbx,
- $cdom,$cnum,$udom,$uname);
+ $cdom,$cnum,$udom,$uname,\%queueable);
}
if ($aggregateflag) {
@@ -5570,6 +6934,9 @@ sub updateGradeByPage {
&Apache::loncommon::end_data_table_row();
$prob++;
+ if ($changeflag) {
+ push(@updates,$symbx);
+ }
}
$curRes = $iterator->next();
}
@@ -5583,6 +6950,76 @@ sub updateGradeByPage {
$hideflag).' ');
$request->print($hidemsg.$grademsg.$studentTable);
+ if (@updates) {
+ undef(@Apache::lonhomework::ltipassback);
+ my (@allsymbs,$mapsymb,@recurseup,%parentmapsymbs,%possmappb,%possrespb);
+ @allsymbs = @updates;
+ if (ref($map)) {
+ $mapsymb = $map->symb();
+ push(@allsymbs,$mapsymb);
+ @recurseup = $navmap->recurseup_maps($map->src,1);
+ }
+ if (@recurseup) {
+ push(@allsymbs,@recurseup);
+ map { $parentmapsymbs{$_} = 1; } @recurseup;
+ }
+ my %passback = &Apache::lonnet::get('nohist_linkprot_passback',\@allsymbs,$cdom,$cnum);
+ my (%uniqsymbs,$use_symbs_in_map);
+ if (keys(%passback)) {
+ foreach my $possible (keys(%passback)) {
+ if (ref($passback{$possible}) eq 'HASH') {
+ if ($possible eq $mapsymb) {
+ foreach my $launcher (keys(%{$passback{$possible}})) {
+ $possmappb{$launcher} = 1;
+ }
+ $use_symbs_in_map = 1;
+ } elsif (exists($parentmapsymbs{$possible})) {
+ foreach my $launcher (keys(%{$passback{$possible}})) {
+ my ($linkuri,$linkprotector,$scope) = split(/\0/,$launcher);
+ if ($scope eq 'rec') {
+ $possmappb{$launcher} = 1;
+ $use_symbs_in_map = 1;
+ }
+ }
+ } elsif (grep(/^\Q$possible$\E$/,@updates)) {
+ foreach my $launcher (keys(%{$passback{$possible}})) {
+ $possrespb{$launcher} = 1;
+ }
+ $uniqsymbs{$possible} = 1;
+ }
+ }
+ }
+ }
+ if ($use_symbs_in_map) {
+ map { $uniqsymbs{$_} = 1; } @symbs_in_map;
+ }
+ my @posslaunchers;
+ if (keys(%possmappb)) {
+ push(@posslaunchers,keys(%possmappb));
+ }
+ if (keys(%possrespb)) {
+ push(@posslaunchers,keys(%possrespb));
+ }
+ if (@posslaunchers) {
+ my (%pbsave,%skip_passback,%needpb);
+ my %pbids = &Apache::lonnet::get('nohist_'.$cdom.'_'.$cnum.'_linkprot_pb',\@posslaunchers,$udom,$uname);
+ foreach my $key (keys(%pbids)) {
+ if (ref($pbids{$key}) eq 'ARRAY') {
+ $needpb{$key} = 1;
+ }
+ }
+ my @symbs = keys(%uniqsymbs);
+ &process_passbacks('updatebypage',\@symbs,$cdom,$cnum,$udom,$uname,\%weights,
+ \%awardeds,\%excuseds,\%needpb,\%skip_passback,\%pbsave,\%pbids);
+ if (@Apache::lonhomework::ltipassback) {
+ unless ($registered_cleanup) {
+ my $handlers = $request->get_handlers('PerlCleanupHandler');
+ $request->set_handlers('PerlCleanupHandler' =>
+ [\&Apache::lonhomework::do_ltipassback,@{$handlers}]);
+ }
+ }
+ }
+ }
return '';
}
@@ -5655,7 +7092,7 @@ the homework problem.
sub defaultFormData {
my ($symb)=@_;
- return '';
+ return '';
}
@@ -5898,8 +7335,7 @@ sub scantron_selectphase {
$ssi_error = 0;
- if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) ||
- &Apache::lonnet::allowed('usc',$env{'request.course.id'})) {
+ if (&Apache::lonnet::allowed('usc',$env{'request.role.domain'}) || $perm{'usc'}) {
# Chunk of form to prompt for a scantron file upload.
@@ -5907,6 +7343,7 @@ sub scantron_selectphase {
');
my $cdom= $env{'course.'.$env{'request.course.id'}.'.domain'};
my $cnum= $env{'course.'.$env{'request.course.id'}.'.num'};
+ my $csec= $env{'request.course.sec'};
my $alertmsg = &mt('Please use the browse button to select a file from your local directory.');
&js_escape(\$alertmsg);
my ($formatoptions,$formattitle,$formatjs) = &scantron_upload_dataformat($cdom);
@@ -5922,6 +7359,7 @@ sub scantron_selectphase {
'.$default_form_data.'
+
'.&Apache::loncommon::start_data_table('LC_scantron_action').'
@@ -6002,8 +7440,6 @@ sub scantron_selectphase {
$r->print($result);
-
-
# Chunk of the form that prompts to view a scoring office file,
# corrected file, skipped records in a file.
@@ -6469,9 +7905,12 @@ sub scantron_parse_scanline {
}
sub get_master_seq {
- my ($resources,$master_seq,$symb_to_resource) = @_;
+ my ($resources,$master_seq,$symb_to_resource,$need_symb_in_map,$symb_for_examcode) = @_;
return unless ((ref($resources) eq 'ARRAY') && (ref($master_seq) eq 'ARRAY') &&
(ref($symb_to_resource) eq 'HASH'));
+ if ($need_symb_in_map) {
+ return unless (ref($symb_for_examcode) eq 'HASH');
+ }
my $resource_error;
foreach my $resource (@{$resources}) {
my $ressymb;
@@ -6479,6 +7918,14 @@ sub get_master_seq {
$ressymb = $resource->symb();
push(@{$master_seq},$ressymb);
$symb_to_resource->{$ressymb} = $resource;
+ if ($need_symb_in_map) {
+ unless ($resource->is_map()) {
+ my $map=(&Apache::lonnet::decode_symb($ressymb))[0];
+ unless (exists($symb_for_examcode->{$map})) {
+ $symb_for_examcode->{$map} = $ressymb;
+ }
+ }
+ }
} else {
$resource_error = 1;
last;
@@ -6981,7 +8428,7 @@ sub scantron_warning_screen {
'
'.&mt('Hand-graded items: points from last bubble in row').'
'.
$env{'form.scantron_lastbubblepoints'}.'
';
}
- return ('
+ return '
'.&mt("Please double check the information below before clicking on '[_1]'",&mt($button_text)).'
@@ -6993,9 +8440,7 @@ sub scantron_warning_screen {
'.&mt("If this information is correct, please click on '[_1]'.",&mt($button_text)).'
'.&mt('If something is incorrect, please return to [_1]Grade/Manage/Review Bubblesheets[_2] to start over.','','').'
'.
+ &Apache::loncommon::end_data_table_row()."\n";
+ }
+ foreach my $sec (sort { $a <=> $b } keys(%bysec)) {
+ next if ($sec eq 'none');
+ $table .= &Apache::loncommon::start_data_table_row().
+ '
'.$sec.'
'.$bysec{$sec}.'
'.
+ &Apache::loncommon::end_data_table_row()."\n";
+ }
+ $table .= &Apache::loncommon::end_data_table()."\n";
+ $gradesections .= &mt('Sections represented in the bubblesheet data file (based on bubbled student IDs) are as follows:').
+ '
'.$table.'
';
+ if (@possibles) {
+ $gradesections .= '
'.
+ &mt('You have role(s) in [quant,_1,other section,other sections] with privileges to manage grades.',
+ scalar(@possibles)).' '.
+ &mt('Check which of those section(s), in addition to section [_1], you wish to grade using this bubblesheet file:',
+ ''.$checksec.'').' ';
+ foreach my $sec (sort {$a <=> $b } @possibles) {
+ $gradesections .= ''.(' 'x2);
+ }
+ $gradesections .= '
';
+ }
+ }
+ } else {
+ $gradesections = '
'.&mt('The selected file is unavailable').'
';
+ }
+ }
my $bubbledbyhand=&hand_bubble_option();
$r->print('
-'.$warning.$bubbledbyhand.'
+'.$warning.$gradesections.$bubbledbyhand.'
');
@@ -7116,7 +8604,38 @@ sub scantron_validate_file {
if ($env{'form.scantron_corrections'}) {
&scantron_process_corrections($r);
}
- $r->print('
'.&mt('Gathering necessary information.').'
');$r->rflush();
+
+ $r->print('
'.&mt('Gathering necessary information.').'
');
+ my ($checksec,@gradable);
+ if ($env{'request.course.sec'}) {
+ ($checksec,my @possibles) = &gradable_sections();
+ if ($checksec) {
+ if (@possibles) {
+ my @chosensecs = &Apache::loncommon::get_env_multiple('form.scantron_othersections');
+ if (@chosensecs) {
+ foreach my $sec (@chosensecs) {
+ if (grep(/^\Q$sec\E$/,@possibles)) {
+ unless (grep(/^\Q$sec\E$/,@gradable)) {
+ push(@gradable,$sec);
+ }
+ }
+ }
+ }
+ }
+ $r->print('
');
+ if (@gradable) {
+ my @showsections = sort { $a <=> $b } (@gradable,$checksec);
+ $r->print(
+ '
'.&mt('Sections to be Graded:').'
'.join(', ',@showsections).'
');
+ } else {
+ $r->print(
+ '
'.&mt('Section to be Graded:').'
'.$checksec.'
');
+ }
+ $r->print('
');
+ }
+ }
+ $r->rflush();
+
#get the student pick code ready
$r->print(&Apache::loncommon::studentbrowser_javascript());
my $nav_error;
@@ -7141,7 +8660,7 @@ sub scantron_validate_file {
$env{'form.validatepass'} = 0;
}
my $currentphase=$env{'form.validatepass'};
-
+ my %skipbysec=();
my $stop=0;
while (!$stop && $currentphase < scalar(@validate_phases)) {
@@ -7151,13 +8670,29 @@ sub scantron_validate_file {
my $which="scantron_validate_".$validate_phases[$currentphase];
{
no strict 'refs';
- ($stop,$currentphase)=&$which($r,$currentphase);
+ my @extras=();
+ if ($validate_phases[$currentphase] eq 'ID') {
+ @extras = (\%skipbysec,$checksec,@gradable);
+ }
+ ($stop,$currentphase)=&$which($r,$currentphase,@extras);
}
}
if (!$stop) {
my $warning=&scantron_warning_screen('Start Grading',$symb);
+ my $secinfo;
+ if (keys(%skipbysec) > 0) {
+ my $seclist = '
';
$onclick = ' onclick="toggleScantab(this.form);"';
$formatjs = <<"END";
@@ -9167,7 +10844,7 @@ END
}
sub scantron_upload_scantron_data_save {
- my($r,$symb)=@_;
+ my ($r,$symb) = @_;
my $doanotherupload=
'
'."\n".
''."\n".
@@ -9175,7 +10852,9 @@ sub scantron_upload_scantron_data_save {
'
'."\n";
if (!&Apache::lonnet::allowed('usc',$env{'form.domainid'}) &&
!&Apache::lonnet::allowed('usc',
- $env{'form.domainid'}.'_'.$env{'form.courseid'})) {
+ $env{'form.domainid'}.'_'.$env{'form.courseid'}) &&
+ !&Apache::lonnet::allowed('usc',
+ $env{'form.domainid'}.'_'.$env{'form.courseid'}.'/'.$env{'form.coursesec'})) {
$r->print(&mt("You are not allowed to upload bubblesheet data to the requested course.")." ");
unless ($symb) {
$r->print($doanotherupload);
@@ -9231,8 +10910,17 @@ sub scantron_upload_scantron_data_save {
(length($env{'form.upfile'})-1),
''.$result.''));
($uploadedfile) = ($result =~ m{/([^/]+)$});
+ if ($uploadedfile =~ /^scantron_orig_/) {
+ my $logname = $uploadedfile;
+ $logname =~ s/^scantron_orig_//;
+ if ($logname ne '') {
+ my $now = time;
+ my %info = ($logname => { $now => $env{'user.name'}.':'.$env{'user.domain'} });
+ &Apache::lonnet::put('scantronupload',\%info,$env{'form.domainid'},$env{'form.courseid'});
+ }
+ }
$r->print(&validate_uploaded_scantron_file($env{'form.domainid'},
- $env{'form.courseid'},$uploadedfile));
+ $env{'form.courseid'},$symb,$uploadedfile));
} else {
$r->print(
&Apache::lonhtmlcommon::confirm_success(&mt('Upload failed'),1).' '.
@@ -9250,13 +10938,34 @@ sub scantron_upload_scantron_data_save {
}
sub validate_uploaded_scantron_file {
- my ($cdom,$cname,$fname) = @_;
+ my ($cdom,$cname,$symb,$fname,$context,$countsref) = @_;
+
my $scanlines=&Apache::lonnet::getfile('/uploaded/'.$cdom.'/'.$cname.'/'.$fname);
my @lines;
if ($scanlines ne '-1') {
@lines=split("\n",$scanlines,-1);
}
- my $output;
+ my ($output,$secidx,$checksec,$priv,%crsroleshash,@possibles);
+ $secidx = &Apache::loncoursedata::CL_SECTION();
+ if ($context eq 'download') {
+ $priv = 'mgr';
+ } else {
+ $priv = 'usc';
+ }
+ unless ((&Apache::lonnet::allowed($priv,$env{'request.role.domain'})) ||
+ (($env{'request.course.id'}) &&
+ (&Apache::lonnet::allowed($priv,$env{'request.course.id'})))) {
+ if ($env{'request.course.sec'} ne '') {
+ unless (&Apache::lonnet::allowed($priv,
+ "$env{'request.course.id'}/$env{'request.course.sec'}")) {
+ unless ($context eq 'download') {
+ $output = '
'.&mt('You do not have permission to upload bubblesheet data').'
';
+ }
+ return $output;
+ }
+ ($checksec,@possibles)=&gradable_sections();
+ }
+ }
if (@lines) {
my (%counts,$max_match_format);
my ($found_match_count,$max_match_count,$max_match_pct) = (0,0,0);
@@ -9269,7 +10978,7 @@ sub validate_uploaded_scantron_file {
my %unique_formats;
my @formatlines = &Apache::lonnet::get_scantronformat_file();
foreach my $line (@formatlines) {
- chomp($line);
+ next if (($line =~ /^\#/) || ($line eq ''));
my @config = split(/:/,$line);
my $idstart = $config[5];
my $idlength = $config[6];
@@ -9286,6 +10995,8 @@ sub validate_uploaded_scantron_file {
%{$counts{$key}} = (
'found' => 0,
'total' => 0,
+ 'totalanysec' => 0,
+ 'othersec' => 0,
);
foreach my $line (@lines) {
next if ($line =~ /^#/);
@@ -9293,6 +11004,23 @@ sub validate_uploaded_scantron_file {
my $id = substr($line,$idstart-1,$idlength);
$id = lc($id);
if (exists($idmap{$id})) {
+ if ($checksec ne '') {
+ $counts{$key}{'totalanysec'} ++;
+ if (ref($classlist->{$idmap{$id}}) eq 'ARRAY') {
+ my $stusec = $classlist->{$idmap{$id}}->[$secidx];
+ if ($stusec ne $checksec) {
+ if (@possibles) {
+ unless (grep(/^\Q$stusec\E$/,@possibles)) {
+ $counts{$key}{'othersec'} ++;
+ next;
+ }
+ } else {
+ $counts{$key}{'othersec'} ++;
+ next;
+ }
+ }
+ }
+ }
$counts{$key}{'found'} ++;
}
$counts{$key}{'total'} ++;
@@ -9307,7 +11035,7 @@ sub validate_uploaded_scantron_file {
}
}
}
- if (ref($unique_formats{$max_match_format}) eq 'ARRAY') {
+ if ((ref($unique_formats{$max_match_format}) eq 'ARRAY') && ($context ne 'download')) {
my $format_descs;
my $numwithformat = @{$unique_formats{$max_match_format}};
for (my $i=0; $i<$numwithformat; $i++) {
@@ -9352,13 +11080,179 @@ sub validate_uploaded_scantron_file {
'
'.&mt('The course roster is not up to date.').'
'.
'';
}
+ if (($checksec ne '') && (ref($counts{$max_match_format}) eq 'HASH')) {
+ if ($counts{$max_match_format}{'othersec'}) {
+ my $percent_nongrade = (100*$counts{$max_match_format}{'othersec'})/($counts{$max_match_format}{'totalanysec'});
+ my $showpct = sprintf("%.0f",$percent_nongrade).'%';
+ my $confirmdel = &mt('Are you sure you want to permanently delete this file?');
+ &js_escape(\$confirmdel);
+ $output .= '
'.
+ &mt('Comparison of student IDs in the uploaded file with the course roster found [_1][quant,_2,match,matches][_3] for students in section(s) for which none of your role(s) have privileges to modify grades',
+ '',$counts{$max_match_format}{'othersec'},'').
+ ' '.
+ &mt('Unless you are assigned role(s) which allow modification of grades in additional sections, [_1] of the records in this file will be automatically excluded when you perform bubblesheet grading.',''.$showpct.'').
+ '
'.
+ &mt('If you prefer to delete the file now, use: [_1]').
+ '
';
}
return $output;
}
+sub gradable_sections {
+ my $checksec = $env{'request.course.sec'};
+ my @oksecs;
+ if ($checksec) {
+ my %availablesecs = §ions_grade_privs();
+ if (ref($availablesecs{'mgr'}) eq 'ARRAY') {
+ foreach my $sec (@{$availablesecs{'mgr'}}) {
+ unless (grep(/^\Q$sec\E$/,@oksecs)) {
+ push(@oksecs,$sec);
+ }
+ }
+ if (grep(/^all$/,@oksecs)) {
+ undef($checksec);
+ }
+ }
+ }
+ return($checksec,@oksecs);
+}
+
+sub sections_grade_privs {
+ my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'};
+ my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'};
+ my %availablesecs = (
+ mgr => [],
+ vgr => [],
+ usc => [],
+ );
+ my $ccrole = 'cc';
+ if ($env{'course.'.$env{'request.course.id'}.'.type'} eq 'Community') {
+ $ccrole = 'co';
+ }
+ my %crsroleshash = &Apache::lonnet::get_my_roles($env{'user.name'},$env{'user.domain'},
+ 'userroles',['active'],
+ [$ccrole,'in','cr'],$cdom,1);
+ my $crsid = $cnum.':'.$cdom;
+ foreach my $item (keys(%crsroleshash)) {
+ next unless ($item =~ /^$crsid\:/);
+ my ($crsnum,$crsdom,$role,$sec) = split(/\:/,$item);
+ my $suffix = "/$cdom/$cnum./$cdom/$cnum";
+ if ($sec ne '') {
+ $suffix = "/$cdom/$cnum/$sec./$cdom/$cnum/$sec";
+ }
+ if (($role eq $ccrole) || ($role eq 'in')) {
+ foreach my $priv ('mgr','vgr','usc') {
+ unless (grep(/^all$/,@{$availablesecs{$priv}})) {
+ if ($sec eq '') {
+ $availablesecs{$priv} = ['all'];
+ } elsif ($sec ne $env{'request.course.sec'}) {
+ unless (grep(/^\Q$sec\E$/,@{$availablesecs{$priv}})) {
+ push(@{$availablesecs{$priv}},$sec);
+ }
+ }
+ }
+ }
+ } elsif ($role =~ m{^cr/}) {
+ foreach my $priv ('mgr','vgr','usc') {
+ unless (grep(/^all$/,@{$availablesecs{$priv}})) {
+ if ($env{"user.priv.$role.$suffix"} =~ /:$priv&/) {
+ if ($sec eq '') {
+ $availablesecs{$priv} = ['all'];
+ } elsif ($sec ne $env{'request.course.sec'}) {
+ unless (grep(/^\Q$sec\E$/,@{$availablesecs{$priv}})) {
+ push(@{$availablesecs{$priv}},$sec);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return %availablesecs;
+}
+
+sub scantron_upload_delete {
+ my ($r,$symb) = @_;
+ my $filename = $env{'form.uploadedfile'};
+ if ($filename =~ /^scantron_orig_/) {
+ if (&Apache::lonnet::allowed('usc',$env{'form.domainid'}) ||
+ &Apache::lonnet::allowed('usc',
+ $env{'form.domainid'}.'_'.$env{'form.courseid'}) ||
+ &Apache::lonnet::allowed('usc',
+ $env{'form.domainid'}.'_'.$env{'form.courseid'}.'/'.$env{'form.coursesec'})) {
+ my $uploadurl = '/uploaded/'.$env{'form.domainid'}.'/'.$env{'form.courseid'}.'/'.$env{'form.uploadedfile'};
+ my $retrieval = &Apache::lonnet::getfile($uploadurl);
+ if ($retrieval eq '-1') {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('File requested for deletion not found.'));
+ } else {
+ $filename =~ s/^scantron_orig_//;
+ if ($filename ne '') {
+ my ($is_valid,$numleft);
+ my %info = &Apache::lonnet::get('scantronupload',[$filename],$env{'form.domainid'},$env{'form.courseid'});
+ if (keys(%info)) {
+ if (ref($info{$filename}) eq 'HASH') {
+ foreach my $timestamp (sort(keys(%{$info{$filename}}))) {
+ if ($info{$filename}{$timestamp} eq $env{'user.name'}.':'.$env{'user.domain'}) {
+ $is_valid = 1;
+ delete($info{$filename}{$timestamp});
+ }
+ }
+ $numleft = scalar(keys(%{$info{$filename}}));
+ }
+ }
+ if ($is_valid) {
+ my $result = &Apache::lonnet::removeuploadedurl($uploadurl);
+ if ($result eq 'ok') {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion successful')).' ');
+ if ($numleft) {
+ &Apache::lonnet::put('scantronupload',\%info,$env{'form.domainid'},$env{'form.courseid'});
+ } else {
+ &Apache::lonnet::del('scantronupload',[$filename],$env{'form.domainid'},$env{'form.courseid'});
+ }
+ } else {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('Result was [_1]',$result));
+ }
+ } else {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('File requested for deletion was uploaded by a different user.'));
+ }
+ } else {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('Filename of bubblesheet data file requested for deletion is invalid.'));
+ }
+ }
+ } else {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('You are not permitted to delete bubblesheet data files from the requested course.'));
+ }
+ } else {
+ $r->print(&Apache::lonhtmlcommon::confirm_success(&mt('File deletion failed'),1).' '.
+ &mt('Filename of bubblesheet data file requested for deletion is invalid.'));
+ }
+ return;
+}
+
sub valid_file {
my ($requested_file)=@_;
foreach my $filename (sort(&scantron_filenames())) {
@@ -9368,7 +11262,7 @@ sub valid_file {
}
sub scantron_download_scantron_data {
- my ($r,$symb)=@_;
+ my ($r,$symb) = @_;
my $default_form_data=&defaultFormData($symb);
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
@@ -9381,6 +11275,29 @@ sub scantron_download_scantron_data {
');
return;
}
+ my (%uploader,$is_owner,%counts,$percent);
+ my %uploader = &Apache::lonnet::get('scantronupload',[$file],$cdom,$cname);
+ if (ref($uploader{$file}) eq 'HASH') {
+ foreach my $timestamp (sort { $a <=> $b } keys(%{$uploader{$file}})) {
+ if ($uploader{$file}{$timestamp} eq $env{'user.name'}.':'.$env{'user.domain'}) {
+ $is_owner = 1;
+ last;
+ }
+ }
+ }
+ unless ($is_owner) {
+ &validate_uploaded_scantron_file($cdom,$cname,$symb,'scantron_orig_'.$file,'download',\%counts);
+ if ($counts{'totalanysec'}) {
+ my $percent_othersec = (100*$counts{'othersec'})/($counts{'totalanysec'});
+ if ($percent_othersec >= 10) {
+ my $showpct = sprintf("%.0f",$percent_othersec).'%';
+ $r->print('
'.
+ &mt('The original uploaded file includes [_1] or more of records for students for which none of your roles have rights to modify grades, so files are unavailable for download.',$showpct).
+ '
');
+ return;
+ }
+ }
+ }
my $orig='/uploaded/'.$cdom.'/'.$cname.'/scantron_orig_'.$file;
my $corrected='/uploaded/'.$cdom.'/'.$cname.'/scantron_corrected_'.$file;
my $skipped='/uploaded/'.$cdom.'/'.$cname.'/scantron_skipped_'.$file;
@@ -9417,7 +11334,7 @@ sub checkscantron_results {
my %scantron_config =
&Apache::lonnet::get_scantron_config($env{'form.scantron_format'});
my $bubbles_per_row = &bubblesheet_bubbles_per_row(\%scantron_config);
- my ($scanlines,$scan_data)=&Apache::grades::scantron_getfile();
+ my ($scanlines,$scan_data)=&scantron_getfile();
my $classlist=&Apache::loncoursedata::get_classlist();
my %idmap=&Apache::grades::username_to_idmap($classlist);
my $navmap=Apache::lonnavmaps::navmap->new();
@@ -9431,6 +11348,17 @@ sub checkscantron_results {
if (ref($map)) {
$randomorder=$map->randomorder();
$randompick=$map->randompick();
+ unless ($randomorder || $randompick) {
+ foreach my $res ($navmap->retrieveResources($map,sub { $_[0]->is_map() },1,0,1)) {
+ if ($res->randomorder()) {
+ $randomorder = 1;
+ }
+ if ($res->randompick()) {
+ $randompick = 1;
+ }
+ last if ($randomorder || $randompick);
+ }
+ }
}
my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
my $nav_error = &get_master_seq(\@resources,\@master_seq,\%symb_to_resource);
@@ -9739,7 +11667,6 @@ sub verify_scantron_grading {
return ($counter,$record);
}
-
#-------- end of section for handling grading scantron forms -------
#
#-------------------------------------------------------------------
@@ -9750,7 +11677,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 {
@@ -9785,37 +11713,47 @@ sub grading_menu {
$fields{'command'} = 'initialverifyreceipt';
my $url5 = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
-
+
+ my %permissions;
+ if ($perm{'mgr'}) {
+ $permissions{'either'} = 'F';
+ $permissions{'mgr'} = 'F';
+ }
+ if ($perm{'vgr'}) {
+ $permissions{'either'} = 'F';
+ $permissions{'vgr'} = 'F';
+ }
+
my @menu = ({ categorytitle=>'Hand Grading',
items =>[
{ linktext => 'Select individual students to grade',
url => $url1a,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'grade_students.png',
linktitle => 'Grade current resource for a selection of students.'
},
{ linktext => 'Grade ungraded submissions',
url => $url1b,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'ungrade_sub.png',
linktitle => 'Grade all submissions that have not been graded yet.'
},
{ linktext => 'Grading table',
url => $url1c,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'grading_table.png',
linktitle => 'Grade current resource for all students.'
},
{ linktext => 'Grade page/folder for one student',
url => $url1d,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'grade_PageFolder.png',
linktitle => 'Grade all resources in current page/sequence/folder for one student.'
},
{ linktext => 'Download submissions',
url => $url1e,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'download_sub.png',
linktitle => 'Download all students submissions.'
}]},
@@ -9824,32 +11762,45 @@ sub grading_menu {
{ linktext => 'Upload Scores',
url => $url2,
- permission => 'F',
+ permission => $permissions{'mgr'},
icon => 'uploadscores.png',
linktitle => 'Specify a file containing the class scores for current resource.'
},
{ linktext => 'Process Clicker',
url => $url3,
- permission => 'F',
+ permission => $permissions{'mgr'},
icon => 'addClickerInfoFile.png',
linktitle => 'Specify a file containing the clicker information for this resource.'
},
{ linktext => 'Grade/Manage/Review Bubblesheets',
url => $url4,
- permission => 'F',
+ permission => $permissions{'mgr'},
icon => 'bubblesheet.png',
linktitle => 'Grade bubblesheet exams, upload/download bubblesheet data files, and review previously graded bubblesheet exams.'
},
{ linktext => 'Verify Receipt Number',
url => $url5,
- permission => 'F',
+ permission => $permissions{'either'},
icon => 'receipt_number.png',
linktitle => 'Verify a system-generated receipt number for correct problem solution.'
}
]
});
-
+ 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 .= '
';
@@ -9860,7 +11811,6 @@ sub grading_menu {
return $Str;
}
-
sub ungraded {
my ($request)=@_;
&submit_options($request);
@@ -9908,11 +11858,30 @@ sub submit_options_download {
my ($request,$symb) = @_;
if (!$symb) {return '';}
+ my $res_error;
+ my ($partlist,$handgrade,$responseType,$numresp,$numessay,$numdropbox) =
+ &response_type($symb,\$res_error);
+ if ($res_error) {
+ $request->print(&mt('An error occurred retrieving response types'));
+ return;
+ }
+ unless ($numessay) {
+ $request->print(&mt('No essayresponse items found'));
+ return;
+ }
+ my $table;
+ if (ref($partlist) eq 'ARRAY') {
+ if (scalar(@$partlist) > 1 ) {
+ $table = &showResourceInfo($symb,$partlist,$responseType,'gradingMenu',1,1);
+ }
+ }
+
my $is_tool = ($symb =~ /ext\.tool$/);
&commonJSfunctions($request);
my $result='
'.&mt('Select Students for whom to Download Submissions').'
@@ -9943,8 +11912,6 @@ sub submit_options {
-
-
';
return $result;
}
@@ -9961,6 +11928,14 @@ sub selectfield {
(&substatus_options,
'select_form_order' => ['yes','queued','graded','incorrect','all']);
}
+
+ #
+ # PrepareClasslist() needs to be called to avoid getting a sections list
+ # for a different course from the @Sections global in lonstatistics.pm,
+ # populated by an earlier request.
+ #
+ &Apache::lonstatistics::PrepareClasslist();
+
my $result='
'.
&Apache::loncommon::end_data_table_row().
@@ -10226,7 +12201,7 @@ ENDPERCFORM
}
sub process_clicker_file {
- my ($r,$symb)=@_;
+ my ($r,$symb) = @_;
if (!$symb) {return '';}
my %Saveable_Parameters=&clicker_grading_parameters();
@@ -10556,7 +12531,7 @@ sub turning_eval {
sub assign_clicker_grades {
- my ($r,$symb)=@_;
+ my ($r,$symb) = @_;
if (!$symb) {return '';}
# See which part we are saving to
my $res_error;
@@ -10567,11 +12542,11 @@ sub assign_clicker_grades {
# FIXME: This should probably look for the first handgradeable part
my $part=$$partlist[0];
# Start screen output
- my $result=&Apache::loncommon::start_data_table().
- &Apache::loncommon::start_data_table_header_row().
- '
');
@@ -11108,8 +13237,8 @@ Side Effects: None.
- missingbubble - array ref of the bubble lines that have missing
bubble errors
- $randomorder - True if exam folder has randomorder set
- $randompick - True if exam folder has randompick set
+ $randomorder - True if exam folder (or a sub-folder) has randomorder set
+ $randompick - True if exam folder (or a sub-folder) has randompick set
$respnumlookup - Reference to HASH mapping question numbers in bubble lines
for current line to question number used for same question
in "Master Seqence" (as seen by Course Coordinator).
@@ -11178,7 +13307,12 @@ Side Effects: None.
=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.
+ has the correct privileges to do so.
+
+= item scantron_upload_delete() :
+
+ Deletes a previously uploaded bubble information data file, if user
+ was the one who uploaded the file, and has the privileges to do so.
=item valid_file() :