--- loncom/homework/grades.pm 2007/06/16 14:09:52 1.412
+++ loncom/homework/grades.pm 2007/09/07 00:07:30 1.439
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.412 2007/06/16 14:09:52 www Exp $
+# $Id: grades.pm,v 1.439 2007/09/07 00:07:30 albertel Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -45,36 +45,26 @@ use LONCAPA;
use POSIX qw(floor);
-my %oldessays=();
+
my %perm=();
+my %bubble_lines_per_response; # no. bubble lines for each response.
+ # index is "symb.part_id"
+
# ----- These first few routines are general use routines.----
#
# --- Retrieve the parts from the metadata file.---
sub getpartlist {
my ($symb) = @_;
- my (undef,undef,$url) = &Apache::lonnet::decode_symb($symb);
- my $partorder = &Apache::lonnet::metadata($url, 'partorder');
- my @parts;
- if ($partorder) {
- for my $part (split (/,/,$partorder)) {
- if (!&Apache::loncommon::check_if_partid_hidden($part,$symb)) {
- push(@parts, $part);
- }
- }
- } else {
- my $metadata = &Apache::lonnet::metadata($url, 'packages');
- foreach (split(/\,/,$metadata)) {
- if ($_ =~ /^part_(.*)$/) {
- if (!&Apache::loncommon::check_if_partid_hidden($1,$symb)) {
- push(@parts, $1);
- }
- }
- }
- }
+
+ my $navmap = Apache::lonnavmaps::navmap->new();
+ my $res = $navmap->getBySymb($symb);
+ my $partlist = $res->parts();
+ my $url = $res->src();
+ my @metakeys = split(/,/,&Apache::lonnet::metadata($url,'keys'));
+
my @stores;
- foreach my $part (@parts) {
- my (@metakeys) = split(/,/,&Apache::lonnet::metadata($url,'keys'));
+ foreach my $part (@{ $partlist }) {
foreach my $key (@metakeys) {
if ($key =~ m/^stores_\Q$part\E_/) { push(@stores,$key); }
}
@@ -93,6 +83,7 @@ sub get_symb {
return ();
}
}
+ &Apache::lonenc::check_decrypt(\$symb);
return ($symb);
}
@@ -194,22 +185,54 @@ sub showResourceInfo {
return $result,$responseType,$hdgrade,$partlist,$handgrade;
}
+sub reset_caches {
+ &reset_analyze_cache();
+ &reset_perm();
+}
-sub get_order {
- my ($partid,$respid,$symb,$uname,$udom)=@_;
- my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
- $url=&Apache::lonnet::clutter($url);
- my $subresult=&Apache::lonnet::ssi($url,
- ('grade_target' => 'analyze'),
- ('grade_domain' => $udom),
- ('grade_symb' => $symb),
- ('grade_courseid' =>
- $env{'request.course.id'}),
- ('grade_username' => $uname));
- (undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
- my %analyze=&Apache::lonnet::str2hash($subresult);
- return ($analyze{"$partid.$respid.shown"});
+{
+ my %analyze_cache;
+
+ sub reset_analyze_cache {
+ undef(%analyze_cache);
+ }
+
+ sub get_analyze {
+ my ($symb,$uname,$udom)=@_;
+ my $key = "$symb\0$uname\0$udom";
+ return $analyze_cache{$key} if (exists($analyze_cache{$key}));
+
+ my (undef,undef,$url)=&Apache::lonnet::decode_symb($symb);
+ $url=&Apache::lonnet::clutter($url);
+ my $subresult=&Apache::lonnet::ssi($url,
+ ('grade_target' => 'analyze'),
+ ('grade_domain' => $udom),
+ ('grade_symb' => $symb),
+ ('grade_courseid' =>
+ $env{'request.course.id'}),
+ ('grade_username' => $uname));
+ (undef,$subresult)=split(/_HASH_REF__/,$subresult,2);
+ my %analyze=&Apache::lonnet::str2hash($subresult);
+ return $analyze_cache{$key} = \%analyze;
+ }
+
+ sub get_order {
+ my ($partid,$respid,$symb,$uname,$udom)=@_;
+ my $analyze = &get_analyze($symb,$uname,$udom);
+ return $analyze->{"$partid.$respid.shown"};
+ }
+
+ sub get_radiobutton_correct_foil {
+ my ($partid,$respid,$symb,$uname,$udom)=@_;
+ my $analyze = &get_analyze($symb,$uname,$udom);
+ foreach my $foil (@{&get_order($partid,$respid,$symb,$uname,$udom)}) {
+ if ($analyze->{"$partid.$respid.foil.value.$foil"} eq 'true') {
+ return $foil;
+ }
+ }
+ }
}
+
#--- Clean response type for display
#--- Currently filters option/rank/radiobutton/match/essay/Task
# response types only.
@@ -258,11 +281,11 @@ sub cleanRecord {
} elsif ($response eq 'radiobutton') {
my %answer=&Apache::lonnet::str2hash($answer);
my ($toprow,$bottomrow);
- my $correct=($order->[0])+1;
- for (my $i=1;$i<=$#$order;$i++) {
- my $foil=$order->[$i];
+ my $correct =
+ &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom);
+ foreach my $foil (@$order) {
if (exists($answer{$foil})) {
- if ($i == $correct) {
+ if ($foil eq $correct) {
$toprow.='
true
';
} else {
$toprow.='
true
';
@@ -492,7 +515,7 @@ sub jscriptNform {
' }'."\n".
''."\n";
$jscript.= ''."\n";
+ 'onClick="javascript:submit();" target="_self" />'."\n";
if (scalar(%$fullname) eq 0) {
my $colspan=3+scalar(@parts);
- $result='There are no students in section "'.$env{'form.section'}.
- '" with enrollment status "'.$env{'form.Status'}.'" to modify or grade.';
+ my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+ $result=''.
+ &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade',
+ $section_display, $env{'form.Status'}).
+ '';
}
$result.=&show_grading_menu_form($symb);
return $result;
@@ -3107,7 +3138,7 @@ sub viewstudentgrade {
''.
"\n".$ctr.'
'."\n";
$student=~s/:/_/; # colon doen't work in javascript for names
foreach my $apart (@$parts) {
@@ -3168,9 +3199,10 @@ sub editgrades {
my ($request) = @_;
my $symb=&get_symb($request);
- my $title='
Current Grade Status
';
- $title.='
Current Resource: '.$env{'form.probTitle'}.'
'."\n";
- $title.='
Section: '.$env{'form.section'}.'
'."\n";
+ my $section_display = join (", ",&Apache::loncommon::get_env_multiple('form.section'));
+ my $title='
'.
@@ -3368,7 +3400,7 @@ sub split_part_type {
my ($partstr) = @_;
my ($temp,@allparts)=split(/_/,$partstr);
my $type=pop(@allparts);
- my $part=join('.',@allparts);
+ my $part=join('_',@allparts);
return ($part,$type);
}
@@ -3471,6 +3503,7 @@ sub csvuploadmap_header {
my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
my $checked=(($env{'form.noFirstLine'})?' checked="checked"':'');
my $ignore=&mt('Ignore First Line');
+ $symb = &Apache::lonenc::check_encrypt($symb);
$request->print(<
Uploading Class Grades
@@ -3565,6 +3598,7 @@ sub upcsvScores_form {
my $upload=&mt("Upload Scores");
my $upfile_select=&Apache::loncommon::upfile_select_html();
my $ignore=&mt('Ignore First Line');
+ $symb = &Apache::lonenc::check_encrypt($symb);
$result.=<
@@ -3839,7 +3873,7 @@ LISTJAVASCRIPT
$result.='
'."\n";
$result.=' Problems from:
SCANTRONFORM
}
+
+ # Chunk of the form that prompts to view a scoring office file,
+ # corrected file, skipped records in a file.
+
$r->print(<
@@ -4602,6 +4778,63 @@ SCANTRONFORM
return
}
+=pod
+
+=item get_scantron_config
+
+ Parse and return the scantron configuration line selected as a
+ hash of configuration file fields.
+
+ Arguments:
+ which - the name of the configuration to parse from the file.
+
+
+ Returns:
+ If the named configuration is not in the file, an empty
+ hash is returned.
+ a hash with the fields
+ name - internal name for the this configuration setup
+ description - text to display to operator that describes this config
+ CODElocation - if 0 or the string 'none'
+ - no CODE exists for this config
+ if -1 || the string 'letter'
+ - a CODE exists for this config and is
+ a string of letters
+ Unsupported value (but planned for future support)
+ if a positive integer
+ - The CODE exists as the first n items from
+ the question section of the form
+ if the string 'number'
+ - The CODE exists for this config and is
+ a string of numbers
+ CODEstart - (only matter if a CODE exists) column in the line where
+ the CODE starts
+ CODElength - length of the CODE
+ IDstart - column where the student ID number starts
+ IDlength - length of the student ID info
+ Qstart - column where the information from the bubbled
+ 'questions' start
+ Qlength - number of columns comprising a single bubble line from
+ the sheet. (usually either 1 or 10)
+ Qon - either a single character representing the character used
+ to signal a bubble was chosen in the positional setup, or
+ the string 'letter' if the letter of the chosen bubble is
+ in the final, or 'number' if a number representing the
+ chosen bubble is in the file (1->A 0->J)
+ Qoff - the character used to represent that a bubble was
+ left blank
+ PaperID - if the scanning process generates a unique number for each
+ sheet scanned the column that this ID number starts in
+ PaperIDlength - number of columns that comprise the unique ID number
+ for the sheet of paper
+ FirstName - column that the first name starts in
+ FirstNameLength - number of columns that the first name spans
+
+ LastName - column that the last name starts in
+ LastNameLength - number of columns that the last name spans
+
+=cut
+
sub get_scantron_config {
my ($which) = @_;
my $fh=Apache::File->new($Apache::lonnet::perlvar{'lonTabDir'}.'/scantronformat.tab');
@@ -4634,6 +4867,25 @@ sub get_scantron_config {
return %config;
}
+=pod
+
+=item username_to_idmap
+
+ creates a hash keyed by student id with values of the corresponding
+ student username:domain.
+
+ Arguments:
+
+ $classlist - reference to the class list hash. This is a hash
+ keyed by student name:domain whose elements are references
+ to arrays containing various chunks of information
+ about the student. (See loncoursedata for more info).
+
+ Returns
+ %idmap - the constructed hash
+
+=cut
+
sub username_to_idmap {
my ($classlist)= @_;
my %idmap;
@@ -4644,8 +4896,50 @@ sub username_to_idmap {
return %idmap;
}
+=pod
+
+=item scantron_fixup_scanline
+
+ Process a requested correction to a scanline.
+
+ Arguments:
+ $scantron_config - hash from &get_scantron_config()
+ $scan_data - hash of correction information
+ (see &scantron_getfile())
+ $line - existing scanline
+ $whichline - line number of the passed in scanline
+ $field - type of change to process
+ (either
+ 'ID' -> correct the student ID number
+ 'CODE' -> correct the CODE
+ 'answer' -> fixup the submitted answers)
+
+ $args - hash of additional info,
+ - 'ID'
+ 'newid' -> studentID to use in replacement
+ of existing one
+ - 'CODE'
+ 'CODE_ignore_dup' - set to true if duplicates
+ should be ignored.
+ 'CODE' - is new code or 'use_unfound'
+ if the existing unfound code should
+ be used as is
+ - 'answer'
+ 'response' - new answer or 'none' if blank
+ 'question' - the bubble line to change
+
+ Returns:
+ $line - the modified scanline
+
+ Side effects:
+ $scan_data - may be updated
+
+=cut
+
+
sub scantron_fixup_scanline {
my ($scantron_config,$scan_data,$line,$whichline,$field,$args)=@_;
+
if ($field eq 'ID') {
if (length($args->{'newid'}) > $$scantron_config{'IDlength'}) {
return ($line,1,'New value too large');
@@ -4702,6 +4996,25 @@ sub scantron_fixup_scanline {
return $line;
}
+=pod
+
+=item scan_data
+
+ Edit or look up an item in the scan_data hash.
+
+ Arguments:
+ $scan_data - The hash (see scantron_getfile)
+ $key - shorthand of the key to edit (actual key is
+ scantronfilename_key).
+ $data - New value of the hash entry.
+ $delete - If true, the entry is removed from the hash.
+
+ Returns:
+ The new value of the hash table field (undefined if deleted).
+
+=cut
+
+
sub scan_data {
my ($scan_data,$key,$value,$delete)=@_;
my $filename=$env{'form.scantron_selectfile'};
@@ -4712,11 +5025,63 @@ sub scan_data {
return $scan_data->{$filename.'_'.$key};
}
+=pod
+
+=item scantron_parse_scanline
+
+ Decodes a scanline from the selected scantron file
+
+ Arguments:
+ line - The text of the scantron file line to process
+ whichline - Line number
+ scantron_config - Hash describing the format of the scantron lines.
+ scan_data - Hash of extra information about the scanline
+ (see scantron_getfile for more information)
+ just_header - True if should not process question answers but only
+ the stuff to the left of the answers.
+ Returns:
+ Hash containing the result of parsing the scanline
+
+ Keys are all proceeded by the string 'scantron.'
+
+ CODE - the CODE in use for this scanline
+ useCODE - 1 if the CODE is invalid but it usage has been forced
+ by the operator
+ CODE_ignore_dup - 1 if the CODE is a duplicated use when unique
+ CODEs were selected, but the usage has been
+ forced by the operator
+ ID - student ID
+ PaperID - if used, the ID number printed on the sheet when the
+ paper was scanned
+ FirstName - first name from the sheet
+ LastName - last name from the sheet
+
+ if just_header was not true these key may also exist
+
+ missingerror - a list of bubbled line numbers that had a blank bubble
+ that is considered an error (if the operator had already
+ okayed a blank bubble line as really being blank then
+ that bubble line number won't appear here.
+ doubleerror - a list of bubbled line numbers that had more than one
+ bubble filled in and has not been corrected by the
+ operator
+ maxquest - the number of the last bubble line that was parsed
+
+ ( starts at 1)
+ .answer - zero or more letters representing the selected
+ letters from the scanline for the bubble line
+ .
+ if blank there was either no bubble or there where
+ multiple bubbles, (consult the keys missingerror and
+ doubleerror if this is an error condition)
+
+=cut
+
sub scantron_parse_scanline {
- my ($line,$whichline,$scantron_config,$scan_data,$justHeader)=@_;
+ my ($line,$whichline,$scantron_config,$scan_data,$just_header)=@_;
my %record;
- my $questions=substr($line,$$scantron_config{'Qstart'}-1);
- my $data=substr($line,0,$$scantron_config{'Qstart'}-1);
+ my $questions=substr($line,$$scantron_config{'Qstart'}-1); # Answers
+ my $data=substr($line,0,$$scantron_config{'Qstart'}-1); # earlier stuff
if (!($$scantron_config{'CODElocation'} eq 0 ||
$$scantron_config{'CODElocation'} eq 'none')) {
if ($$scantron_config{'CODElocation'} < 0 ||
@@ -4746,7 +5111,7 @@ sub scantron_parse_scanline {
$record{'scantron.LastName'}=
substr($data,$$scantron_config{'LastName'}-1,
$$scantron_config{'LastNamelength'});
- if ($justHeader) { return \%record; }
+ if ($just_header) { return \%record; }
my @alphabet=('A'..'Z');
my $questnum=0;
@@ -4819,6 +5184,24 @@ sub scantron_parse_scanline {
return \%record;
}
+=pod
+
+=item scantron_add_delay
+
+ Adds an error message that occurred during the grading phase to a
+ queue of messages to be shown after grading pass is complete
+
+ Arguments:
+ $delayqueue - arrary ref of hash ref of error messages
+ $scanline - the scanline that caused the error
+ $errormesage - the error message
+ $errorcode - a numeric code for the error
+
+ Side Effects:
+ updates the $delayqueue to have a new hash ref of the error
+
+=cut
+
sub scantron_add_delay {
my ($delayqueue,$scanline,$errormessage,$errorcode)=@_;
push(@$delayqueue,
@@ -4827,6 +5210,24 @@ sub scantron_add_delay {
);
}
+=pod
+
+=item scantron_find_student
+
+ Finds the username for the current scanline
+
+ Arguments:
+ $scantron_record - hash result from scantron_parse_scanline
+ $scan_data - hash of correction information
+ (see &scantron_getfile() form more information)
+ $idmap - hash from &username_to_idmap()
+ $line - number of current scanline
+
+ Returns:
+ Either 'username:domain' or undef if unknown
+
+=cut
+
sub scantron_find_student {
my ($scantron_record,$scan_data,$idmap,$line)=@_;
my $scanID=$$scantron_record{'scantron.ID'};
@@ -4841,6 +5242,15 @@ sub scantron_find_student {
return undef;
}
+=pod
+
+=item scantron_filter
+
+ Filter sub for lonnavmaps, filters out hidden resources if ignore
+ hidden resources was selected
+
+=cut
+
sub scantron_filter {
my ($curres)=@_;
@@ -4857,6 +5267,15 @@ sub scantron_filter {
return 0;
}
+=pod
+
+=item scantron_process_corrections
+
+ Gets correction information out of submitted form data and corrects
+ the scanline
+
+=cut
+
sub scantron_process_corrections {
my ($r) = @_;
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -4914,12 +5333,30 @@ sub scantron_process_corrections {
}
}
+=pod
+
+=item reset_skipping_status
+
+ Forgets the current set of remember skipped scanlines (and thus
+ reverts back to considering all lines in the
+ scantron_skipped_ file)
+
+=cut
+
sub reset_skipping_status {
my ($scanlines,$scan_data)=&scantron_getfile();
&scan_data($scan_data,'remember_skipping',undef,1);
&scantron_putfile(undef,$scan_data);
}
+=pod
+
+=item start_skipping
+
+ Marks a scanline to be skipped.
+
+=cut
+
sub start_skipping {
my ($scan_data,$i)=@_;
my %remembered=split(':',&scan_data($scan_data,'remember_skipping'));
@@ -4931,6 +5368,14 @@ sub start_skipping {
&scan_data($scan_data,'remember_skipping',join(':',%remembered));
}
+=pod
+
+=item should_be_skipped
+
+ Checks whether a scanline should be skipped.
+
+=cut
+
sub should_be_skipped {
my ($scanlines,$scan_data,$i)=@_;
if ($env{'form.scantron_options_redo'} !~ /^redo_/) {
@@ -4946,6 +5391,15 @@ sub should_be_skipped {
return 1;
}
+=pod
+
+=item remember_current_skipped
+
+ Discovers what scanlines are in the scantron_skipped_
+ file and remembers them into scan_data for later use.
+
+=cut
+
sub remember_current_skipped {
my ($scanlines,$scan_data)=&scantron_getfile();
my %to_remember;
@@ -4959,6 +5413,16 @@ sub remember_current_skipped {
&scantron_putfile(undef,$scan_data);
}
+=pod
+
+=item check_for_error
+
+ Checks if there was an error when attempting to remove a specific
+ scantron_.. bubble sheet data file. Prints out an error if
+ something went wrong.
+
+=cut
+
sub check_for_error {
my ($r,$result)=@_;
if ($result ne 'ok' && $result ne 'not_found' ) {
@@ -4966,6 +5430,15 @@ sub check_for_error {
}
}
+=pod
+
+=item scantron_warning_screen
+
+ Interstitial screen to make sure the operator has selected the
+ correct options before we start the validation phase.
+
+=cut
+
sub scantron_warning_screen {
my ($button_text)=@_;
my $title=&Apache::lonnet::gettitle($env{'form.selectpage'});
@@ -4998,6 +5471,15 @@ $CODElist
STUFF
}
+=pod
+
+=item scantron_do_warning
+
+ Check if the operator has picked something for all required
+ fields. Error out if something is missing.
+
+=cut
+
sub scantron_do_warning {
my ($r)=@_;
my ($symb)=&get_symb($r);
@@ -5029,6 +5511,14 @@ STUFF
return '';
}
+=pod
+
+=item scantron_form_start
+
+ html hidden input for remembering all selected grading options
+
+=cut
+
sub scantron_form_start {
my ($max_bubble)=@_;
my $result= <print("
Gathering neccessary info.
");$r->rflush();
+ $r->print("
Gathering necessary info.
");$r->rflush();
#get the student pick code ready
$r->print(&Apache::loncommon::studentbrowser_javascript());
my $max_bubble=&scantron_get_maxbubble();
@@ -5129,6 +5631,17 @@ STUFF
return '';
}
+
+=pod
+
+=item scantron_remove_file
+
+ Removes the requested bubble sheet data file, makes sure that
+ scantron_original_ is never removed
+
+
+=cut
+
sub scantron_remove_file {
my ($which)=@_;
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
@@ -5143,6 +5656,18 @@ sub scantron_remove_file {
return &Apache::lonnet::removeuserfile($cname,$cdom,$file);
}
+
+=pod
+
+=item scantron_remove_scan_data
+
+ Removes all scan_data correction for the requested bubble sheet
+ data file. (In the case that both the are doing skipped records we need
+ to remember the old skipped lines for the time being so that element
+ persists for a while.)
+
+=cut
+
sub scantron_remove_scan_data {
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
@@ -5165,6 +5690,51 @@ sub scantron_remove_scan_data {
return $result;
}
+
+=pod
+
+=item scantron_getfile
+
+ Fetches the requested bubble sheet data file (all 3 versions), and
+ the scan_data hash
+
+ Arguments:
+ None
+
+ Returns:
+ 2 hash references
+
+ - first one has
+ orig -
+ corrected -
+ skipped - each of which points to an array ref of the specified
+ file broken up into individual lines
+ count - number of scanlines
+
+ - second is the scan_data hash possible keys are
+ ($number refers to scanline numbered $number and thus the key affects
+ only that scanline
+ $bubline refers to the specific bubble line element and the aspects
+ refers to that specific bubble line element)
+
+ $number.user - username:domain to use
+ $number.CODE_ignore_dup
+ - ignore the duplicate CODE error
+ $number.useCODE
+ - use the CODE in the scanline as is
+ $number.no_bubble.$bubline
+ - it is valid that there is no bubbled in bubble
+ at $number $bubline
+ remember_skipping
+ - a frozen hash containing keys of $number and values
+ of either
+ 1 - we are on a 'do skipped records pass' and plan
+ on processing this line
+ 2 - we are on a 'do skipped records pass' and this
+ scanline has been marked to skip yet again
+
+=cut
+
sub scantron_getfile {
#FIXME really would prefer a scantron directory
my $cname=$env{'course.'.$env{'request.course.id'}.'.num'};
@@ -5197,6 +5767,21 @@ sub scantron_getfile {
return (\%scanlines,\%scan_data);
}
+=pod
+
+=item lonnet_putfile
+
+ Wrapper routine to call &Apache::lonnet::finishuserfileupload
+
+ Arguments:
+ $contents - data to store
+ $filename - filename to store $contents into
+
+ Returns:
+ result value from &Apache::lonnet::finishuserfileupload
+
+=cut
+
sub lonnet_putfile {
my ($contents,$filename)=@_;
my $docuname=$env{'course.'.$env{'request.course.id'}.'.num'};
@@ -5206,6 +5791,22 @@ sub lonnet_putfile {
}
+=pod
+
+=item scantron_putfile
+
+ Stores the current version of the bubble sheet data files, and the
+ scan_data hash. (Does not modify the original version only the
+ corrected and skipped versions.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+
+=cut
+
sub scantron_putfile {
my ($scanlines,$scan_data) = @_;
#FIXME really would prefer a scantron directory
@@ -5226,6 +5827,28 @@ sub scantron_putfile {
&Apache::lonnet::put('nohist_scantrondata',$scan_data,$cdom,$cname);
}
+=pod
+
+=item scantron_get_line
+
+ Returns the correct version of the scanline
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - number of the requested line (starts at 0)
+
+ Returns:
+ A scanline, (either the original or the corrected one if it
+ exists), or undef if the requested scanline should be
+ skipped. (Either because it's an skipped scanline, or it's an
+ unskipped scanline and we are not doing a 'do skipped scanlines'
+ pass.
+
+=cut
+
sub scantron_get_line {
my ($scanlines,$scan_data,$i)=@_;
if (&should_be_skipped($scanlines,$scan_data,$i)) { return undef; }
@@ -5234,6 +5857,23 @@ sub scantron_get_line {
return $scanlines->{'orig'}[$i];
}
+=pod
+
+=item scantron_todo_count
+
+ Counts the number of scanlines that need processing.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+
+ Returns:
+ $count - number of scanlines to process
+
+=cut
+
sub get_todo_count {
my ($scanlines,$scan_data)=@_;
my $count=0;
@@ -5245,6 +5885,25 @@ sub get_todo_count {
return $count;
}
+=pod
+
+=item scantron_put_line
+
+ Updates the 'corrected' or 'skipped' versions of the bubble sheet
+ data file.
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - line number to update
+ $newline - contents of the updated scanline
+ $skip - if true make the line for skipping and update the
+ 'skipped' file
+
+=cut
+
sub scantron_put_line {
my ($scanlines,$scan_data,$i,$newline,$skip)=@_;
if ($skip) {
@@ -5255,6 +5914,21 @@ sub scantron_put_line {
$scanlines->{'corrected'}[$i]=$newline;
}
+=pod
+
+=item scantron_clear_skip
+
+ Remove a line from the 'skipped' file
+
+ Arguments:
+ $scanlines - hash ref that looks like the first return value from
+ &scantron_getfile()
+ $scan_data - hash ref that looks like the second return value from
+ &scantron_getfile()
+ $i - line number to update
+
+=cut
+
sub scantron_clear_skip {
my ($scanlines,$scan_data,$i)=@_;
if (exists($scanlines->{'skipped'}[$i])) {
@@ -5264,6 +5938,15 @@ sub scantron_clear_skip {
return 0;
}
+=pod
+
+=item scantron_filter_not_exam
+
+ Filter routine used by &Apache::lonnavmaps::retrieveResources(), to
+ filter out resources that are not marked as 'exam' mode
+
+=cut
+
sub scantron_filter_not_exam {
my ($curres)=@_;
@@ -5280,6 +5963,15 @@ sub scantron_filter_not_exam {
return 0;
}
+=pod
+
+=item scantron_validate_sequence
+
+ Validates the selected sequence, checking for resource that are
+ not set to exam mode.
+
+=cut
+
sub scantron_validate_sequence {
my ($r,$currentphase) = @_;
@@ -5303,6 +5995,15 @@ sub scantron_validate_sequence {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_validate_ID
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified student IDs
+
+=cut
+
sub scantron_validate_ID {
my ($r,$currentphase) = @_;
@@ -5365,6 +6066,36 @@ sub scantron_validate_ID {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_get_correction
+
+ Builds the interface screen to interact with the operator to fix a
+ specific error condition in a specific scanline
+
+ Arguments:
+ $r - Apache request object
+ $i - number of the current scanline
+ $scan_record - hash ref as returned from &scantron_parse_scanline()
+ $scan_config - hash ref as returned from &get_scantron_config()
+ $line - full contents of the current scanline
+ $error - error condition, valid values are
+ 'incorrectCODE', 'duplicateCODE',
+ 'doublebubble', 'missingbubble',
+ 'duplicateID', 'incorrectID'
+ $arg - extra information needed
+ For errors:
+ - duplicateID - paper number that this studentID was seen before on
+ - duplicateCODE - array ref of the paper numbers this CODE was
+ seen on before
+ - incorrectCODE - current incorrect CODE
+ - doublebubble - array ref of the bubble lines that have double
+ bubble errors
+ - missingbubble - array ref of the bubble lines that have missing
+ bubble errors
+
+=cut
+
sub scantron_get_correction {
my ($r,$i,$scan_record,$scan_config,$line,$error,$arg)=@_;
@@ -5468,7 +6199,8 @@ ENDSCRIPT
$r->print("
Please indicate which bubble should be used for grading
There have been no bubbles scanned for some question(s)
\n");
@@ -5488,32 +6220,99 @@ ENDSCRIPT
}
+=pod
+
+=item scantron_bubble_selector
+
+ Generates the html radiobuttons to correct a single bubble line
+ possibly showing the existing the selected bubbles if known
+
+ Arguments:
+ $r - Apache request object
+ $scan_config - hash from &get_scantron_config()
+ $quest - number of the bubble line to make a corrector for
+ $selected - array of letters of previously selected bubbles
+ $lines - if present, number of bubble lines to show
+
+=cut
+
sub scantron_bubble_selector {
- my ($r,$scan_config,$quest,@selected)=@_;
+ my ($r,$scan_config,$quest,@selected, $lines)=@_;
my $max=$$scan_config{'Qlength'};
my $scmode=$$scan_config{'Qon'};
if ($scmode eq 'number' || $scmode eq 'letter') { $max=10; }
- my @alphabet=('A'..'Z');
- $r->print("
$quest
");
- for (my $i=0;$i<$max+1;$i++) {
- $r->print("\n".'
');
+
+ # FIXME: This may have to be a bit more clever for
+ # multiline questions (different values e.g..).
+
+ for (my $i=0;$i<$max;$i++) {
+ $r->print("\n".
+ '
'.$alphabet[$i]."
");
+ }
+ $r->print('
');
+
+
+ }
+ $r->print('
');
}
+=pod
+
+=item num_matches
+
+ Counts the number of characters that are the same between the two arguments.
+
+ Arguments:
+ $orig - CODE from the scanline
+ $code - CODE to match against
+
+ Returns:
+ $count - integer count of the number of same characters between the
+ two arguments
+
+=cut
+
sub num_matches {
my ($orig,$code) = @_;
my @code=split(//,$code);
@@ -5525,6 +6324,26 @@ sub num_matches {
return $same;
}
+=pod
+
+=item scantron_get_closely_matching_CODEs
+
+ Cycles through all CODEs and finds the set that has the greatest
+ number of same characters as the provided CODE
+
+ Arguments:
+ $allcodes - hash ref returned by &get_codes()
+ $CODE - CODE from the current scanline
+
+ Returns:
+ 2 element list
+ - first elements is number of how closely matching the best fit is
+ (5 means best set has 5 matching characters)
+ - second element is an arrary ref containing the set of valid CODEs
+ that best fit the passed in CODE
+
+=cut
+
sub scantron_get_closely_matching_CODEs {
my ($allcodes,$CODE)=@_;
my @CODEs;
@@ -5535,6 +6354,23 @@ sub scantron_get_closely_matching_CODEs
return ($#CODEs,$CODEs[-1]);
}
+=pod
+
+=item get_codes
+
+ Builds a hash which has keys of all of the valid CODEs from the selected
+ set of remembered CODEs.
+
+ Arguments:
+ $old_name - name of the set of remembered CODEs
+ $cdom - domain of the course
+ $cnum - internal course name
+
+ Returns:
+ %allcodes - keys are the valid CODEs, values are all 1
+
+=cut
+
sub get_codes {
my ($old_name, $cdom, $cnum) = @_;
if (!$old_name) {
@@ -5557,6 +6393,16 @@ sub get_codes {
return %allcodes;
}
+=pod
+
+=item scantron_validate_CODE
+
+ Validates all scanlines in the selected file to not have any
+ invalid or underspecified CODEs and that none of the codes are
+ duplicated if this was requested.
+
+=cut
+
sub scantron_validate_CODE {
my ($r,$currentphase) = @_;
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
@@ -5608,6 +6454,15 @@ sub scantron_validate_CODE {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_validate_doublebubble
+
+ Validates all scanlines in the selected file to not have any
+ bubble lines with multiple bubbles marked.
+
+=cut
+
sub scantron_validate_doublebubble {
my ($r,$currentphase) = @_;
#get student info
@@ -5631,7 +6486,21 @@ sub scantron_validate_doublebubble {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_get_maxbubble
+
+ Returns the maximum number of bubble lines that are expected to
+ occur. Does this by walking the selected sequence rendering the
+ resource and then checking &Apache::lonxml::get_problem_counter()
+ for what the current value of the problem counter is.
+
+ Caches the result to $env{'form.scantron_maxbubble'}
+
+=cut
+
sub scantron_get_maxbubble {
+
if (defined($env{'form.scantron_maxbubble'}) &&
$env{'form.scantron_maxbubble'}) {
return $env{'form.scantron_maxbubble'};
@@ -5646,17 +6515,52 @@ sub scantron_get_maxbubble {
&Apache::lonxml::clear_problem_counter();
+ my $uname = $env{'form.student'};
+ my $udom = $env{'form.userdom'};
+ my $cid = $env{'request.course.id'};
+ my $total_lines = 0;
+ %bubble_lines_per_response = ();
+
foreach my $resource (@resources) {
+ my $symb = $resource->symb();
my $result=&Apache::lonnet::ssi($resource->src(),
- ('symb' => $resource->symb()));
+ ('symb' => $resource->symb()),
+ ('grade_target' => 'analyze'),
+ ('grade_courseid' => $cid),
+ ('grade_domain' => $udom),
+ ('grade_username' => $uname));
+ my (undef, $an) =
+ split(/_HASH_REF__/,$result, 2);
+
+ my %analysis = &Apache::lonnet::str2hash($an);
+
+
+
+ foreach my $part_id (@{$analysis{'parts'}}) {
+ my $bubble_lines = $analysis{"$part_id.bubble_lines"}[0];
+ if (!$bubble_lines) {
+ $bubble_lines = 1;
+ }
+ $bubble_lines_per_response{"$symb.$part_id"} = $bubble_lines;
+ $total_lines = $total_lines + $bubble_lines;
+ }
+
}
&Apache::lonnet::delenv('scantron\.');
$env{'form.scantron_maxbubble'} =
- &Apache::lonxml::get_problem_counter()-1;
-
+ $total_lines;
return $env{'form.scantron_maxbubble'};
}
+=pod
+
+=item scantron_validate_missingbubbles
+
+ Validates all scanlines in the selected file to not have any
+ bubble lines with missing bubbles that haven't been verified as missing.
+
+=cut
+
sub scantron_validate_missingbubbles {
my ($r,$currentphase) = @_;
#get student info
@@ -5689,6 +6593,30 @@ sub scantron_validate_missingbubbles {
return (0,$currentphase+1);
}
+=pod
+
+=item scantron_process_students
+
+ Routine that does the actual grading of the bubble sheet information.
+
+ The parsed scanline hash is added to %env
+
+ Then foreach unskipped scanline it does an &Apache::lonnet::ssi()
+ foreach resource , with the form data of
+
+ 'submitted' =>'scantron'
+ 'grade_target' =>'grade',
+ 'grade_username'=> username of student
+ 'grade_domain' => domain of student
+ 'grade_courseid'=> of course
+ 'grade_symb' => symb of resource to grade
+
+ This triggers a grading pass. The problem grading code takes care
+ of converting the bubbled letter information (now in %env) into a
+ valid submission.
+
+=cut
+
sub scantron_process_students {
my ($r) = @_;
my (undef,undef,$sequence)=&Apache::lonnet::decode_symb($env{'form.selectpage'});
@@ -5793,6 +6721,14 @@ SCANTRONFORM
return '';
}
+=pod
+
+=item scantron_upload_scantron_data
+
+ Creates the screen for adding a new bubble sheet data file to a course.
+
+=cut
+
sub scantron_upload_scantron_data {
my ($r)=@_;
$r->print(&Apache::loncommon::coursebrowser_javascript($env{'request.role.domain'}));
@@ -5829,6 +6765,15 @@ UPLOAD
return '';
}
+=pod
+
+=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.
+
+=cut
+
sub scantron_upload_scantron_data_save {
my($r)=@_;
my ($symb)=&get_symb($r,1);
@@ -5884,6 +6829,14 @@ sub scantron_upload_scantron_data_save {
return '';
}
+=pod
+
+=item valid_file
+
+ Validates that the requested bubble data file exists in the course.
+
+=cut
+
sub valid_file {
my ($requested_file)=@_;
foreach my $filename (sort(&scantron_filenames())) {
@@ -5892,6 +6845,16 @@ sub valid_file {
return 0;
}
+=pod
+
+=item scantron_download_scantron_data
+
+ Shows a list of the three internal files (original, corrected,
+ skipped) for a specific bubble sheet data file that exists in the
+ course.
+
+=cut
+
sub scantron_download_scantron_data {
my ($r)=@_;
my $default_form_data=&defaultFormData(&get_symb($r,1));
@@ -5928,6 +6891,12 @@ DOWNLOAD
return '';
}
+=pod
+
+=back
+
+=cut
+
#-------- end of section for handling grading scantron forms -------
#
#-------------------------------------------------------------------
@@ -5938,7 +6907,7 @@ DOWNLOAD
sub show_grading_menu_form {
my ($symb)=@_;
my $result.='
'."\n";
+# Attempt to restore parameters from last session, set defaults if not present
+ my %Saveable_Parameters=&clicker_grading_parameters();
+ &Apache::loncommon::restore_course_settings('grades_clicker',
+ \%Saveable_Parameters);
+ if (!$env{'form.pcorrect'}) { $env{'form.pcorrect'}=100; }
+ if (!$env{'form.pincorrect'}) { $env{'form.pincorrect'}=100; }
+ if (!$env{'form.gradingmechanism'}) { $env{'form.gradingmechanism'}='attendance'; }
+ if (!$env{'form.upfiletype'}) { $env{'form.upfiletype'}='iclicker'; }
+
+ my %checked;
+ foreach my $gradingmechanism ('attendance','personnel','specific') {
+ if ($env{'form.gradingmechanism'} eq $gradingmechanism) {
+ $checked{$gradingmechanism}="checked='checked'";
+ }
+ }
+
my $upload=&mt("Upload File");
my $type=&mt("Type");
my $attendance=&mt("Award points just for participation");
my $personnel=&mt("Correctness determined from response by course personnel");
- my $specific=&mt("Correctness determined from response with clicker ID");
+ my $specific=&mt("Correctness determined from response with clicker ID(s)");
my $pcorrect=&mt("Percentage points for correct solution");
my $pincorrect=&mt("Percentage points for incorrect solution");
- my $selectform=&Apache::loncommon::select_form('iclicker','upfiletype',
- ('iclicker' => 'i>clicker'));
-
+ my $selectform=&Apache::loncommon::select_form($env{'form.upfiletype'},'upfiletype',
+ ('iclicker' => 'i>clicker',
+ 'interwrite' => 'interwrite PRS'));
+ $symb = &Apache::lonenc::check_encrypt($symb);
$result.=<
function sanitycheck() {
@@ -6248,13 +7245,13 @@ function sanitycheck() {
$type: $selectform
- $attendance:
- $personnel:
- $specific:
-
-
- $pcorrect:
- $pincorrect:
+ $attendance:
+ $personnel:
+ $specific:
+
+
+ $pcorrect:
+ $pincorrect:
ENDUPFORM
@@ -6268,6 +7265,11 @@ sub process_clicker_file {
my ($r)=@_;
my ($symb)=&get_symb($r);
if (!$symb) {return '';}
+
+ my %Saveable_Parameters=&clicker_grading_parameters();
+ &Apache::loncommon::store_course_settings('grades_clicker',
+ \%Saveable_Parameters);
+
my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
if (($env{'form.gradingmechanism'} eq 'specific') && ($env{'form.specificid'}!~/\w/)) {
$result.=''.&mt('You need to specify a clicker ID for the correct answer').'';
@@ -6279,11 +7281,15 @@ sub process_clicker_file {
%correct_ids=&gather_adv_clicker_ids();
}
if ($env{'form.gradingmechanism'} eq 'specific') {
- my $correct_id=$env{'form.specificid'};
- $correct_id=~tr/a-z/A-Z/;
- $correct_id=~s/\s//gs;
- $correct_id=~s/^0+//;
- $correct_ids{$correct_id}='specified';
+ foreach my $correct_id (split(/[\s\,]/,$env{'form.specificid'})) {;
+ $correct_id=~tr/a-z/A-Z/;
+ $correct_id=~s/\s//gs;
+ $correct_id=~s/^[\#0]+//;
+ $correct_id=~s/[\-\:]//g;
+ if ($correct_id) {
+ $correct_ids{$correct_id}='specified';
+ }
+ }
}
if ($env{'form.gradingmechanism'} eq 'attendance') {
$result.=&mt('Score based on attendance only');
@@ -6317,6 +7323,7 @@ sub process_clicker_file {
# Were able to get all the info needed, now analyze the file
$result.=&Apache::loncommon::studentbrowser_javascript();
+ $symb = &Apache::lonenc::check_encrypt($symb);
my $heading=&mt('Scanning clicker file');
$result.=(<
@@ -6338,21 +7345,45 @@ ENDHEADER
if ($env{'form.upfiletype'} eq 'iclicker') {
($errormsg,$number)=&iclicker_eval(\@questiontitles,\%responses);
}
+ if ($env{'form.upfiletype'} eq 'interwrite') {
+ ($errormsg,$number)=&interwrite_eval(\@questiontitles,\%responses);
+ }
$result.=' '.&mt('Found [_1] question(s)',$number).' '.
''.
&mt('Awarding [_1] percent for correct and [_2] percent for incorrect responses',
$env{'form.pcorrect'},$env{'form.pincorrect'}).
' ';
+# Remember Question Titles
+# FIXME: Possibly need delimiter other than ":"
+ for (my $i=0;$i<$number;$i++) {
+ $result.='').'" />';
+ }
my $correct_count=0;
my $student_count=0;
my $unknown_count=0;
+# Match answers with usernames
+# FIXME: Possibly need delimiter other than ":"
foreach my $id (keys(%responses)) {
if ($correct_ids{$id}) {
- $result.="\n".'';
+ $result.="\n".'';
$correct_count++;
} elsif ($clicker_ids{$id}) {
- $result.="\n".'';
- $student_count++;
+ if ($clicker_ids{$id}=~/\,/) {
+# More than one user with the same clicker!
+ $result.="\n".&mt('Clicker registered more than once').": ".$id." ";
+ $result.="\n".''.
+ "";
+ foreach my $reguser (sort(split(/\,/,$clicker_ids{$id}))) {
+ $result.="';
+ }
+ $result.='';
+ $unknown_count++;
+ } else {
+# Good: found one and only one user with the right clicker
+ $result.="\n".'';
+ $student_count++;
+ }
} else {
$result.="\n".&mt('Unregistered Clicker')." ".$id." ";
$result.="\n".''.
@@ -6369,9 +7400,12 @@ ENDHEADER
if ($correct_count==0) {
$errormsg.="Found no correct answers answers for grading!";
} elsif ($correct_count>1) {
- $result.=' '.&mt("Found [_1] entries for grading!").'';
+ $result.=' '.&mt("Found [_1] entries for grading!",$correct_count).'';
}
}
+ if ($number<1) {
+ $errormsg.="Found no questions.";
+ }
if ($errormsg) {
$result.=' '.&mt($errormsg).'';
} else {
@@ -6409,10 +7443,145 @@ sub iclicker_eval {
return ($errormsg,$number);
}
+sub interwrite_eval {
+ my ($questiontitles,$responses)=@_;
+ my $number=0;
+ my $errormsg='';
+ my $skipline=1;
+ my $questionnumber=0;
+ my %idresponses=();
+ foreach my $line (split(/[\n\r]/,$env{'form.upfile'})) {
+ my %components=&Apache::loncommon::record_sep($line);
+ my @entries=map {$components{$_}} (sort(keys(%components)));
+ if ($entries[1] eq 'Time') { $skipline=0; next; }
+ if ($entries[1] eq 'Response') { $skipline=1; }
+ next if $skipline;
+ if ($entries[0]!=$questionnumber) {
+ $questionnumber=$entries[0];
+ $$questiontitles[$number]=&mt('Question [_1]',$questionnumber);
+ $number++;
+ }
+ my $id=$entries[4];
+ $id=~s/^[\#0]+//;
+ $id=~s/^v\d*\://i;
+ $id=~s/[\-\:]//g;
+ $idresponses{$id}[$number]=$entries[6];
+ }
+ foreach my $id (keys %idresponses) {
+ $$responses{$id}=join(',',@{$idresponses{$id}});
+ $$responses{$id}=~s/^\s*\,//;
+ }
+ return ($errormsg,$number);
+}
+
+sub assign_clicker_grades {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+# See which part we are saving to
+ my ($partlist,$handgrade,$responseType) = &response_type($symb);
+# FIXME: This should probably look for the first handgradeable part
+ my $part=$$partlist[0];
+# Start screen output
+ my ($result) = &showResourceInfo($symb,$env{'form.probTitle'});
+
+ my $heading=&mt('Assigning grades based on clicker file');
+ $result.=(<
+
+$heading
+ENDHEADER
+# Get correct result
+# FIXME: Possibly need delimiter other than ":"
+ my @correct=();
+ my $gradingmechanism=$env{'form.gradingmechanism'};
+ my $number=$env{'form.number'};
+ if ($gradingmechanism ne 'attendance') {
+ foreach my $key (keys(%env)) {
+ if ($key=~/^form\.correct\:/) {
+ my @input=split(/\,/,$env{$key});
+ for (my $i=0;$i<=$#input;$i++) {
+ if (($correct[$i]) && ($input[$i]) &&
+ ($correct[$i] ne $input[$i])) {
+ $result.=' '.
+ &mt('More than one correct result given for question "[_1]": [_2] versus [_3].',
+ $env{'form.question:'.$i},$correct[$i],$input[$i]).'';
+ } elsif ($input[$i]) {
+ $correct[$i]=$input[$i];
+ }
+ }
+ }
+ }
+ for (my $i=0;$i<$number;$i++) {
+ if (!$correct[$i]) {
+ $result.=' '.
+ &mt('No correct result given for question "[_1]"!',
+ $env{'form.question:'.$i}).'';
+ }
+ }
+ $result.=' '.&mt("Correct answer: [_1]",join(', ',map { ($_?$_:'-') } @correct));
+ }
+# Start grading
+ my $pcorrect=$env{'form.pcorrect'};
+ my $pincorrect=$env{'form.pincorrect'};
+ my $storecount=0;
+ foreach my $key (keys(%env)) {
+ my $user='';
+ if ($key=~/^form\.student\:(.*)$/) {
+ $user=$1;
+ }
+ if ($key=~/^form\.unknown\:(.*)$/) {
+ my $id=$1;
+ if (($env{'form.uname'.$id}) && ($env{'form.udom'.$id})) {
+ $user=$env{'form.uname'.$id}.':'.$env{'form.udom'.$id};
+ } elsif ($env{'form.multi'.$id}) {
+ $user=$env{'form.multi'.$id};
+ }
+ }
+ if ($user) {
+ my @answer=split(/\,/,$env{$key});
+ my $sum=0;
+ for (my $i=0;$i<$number;$i++) {
+ if ($answer[$i]) {
+ if ($gradingmechanism eq 'attendance') {
+ $sum+=$pcorrect;
+ } else {
+ if ($answer[$i] eq $correct[$i]) {
+ $sum+=$pcorrect;
+ } else {
+ $sum+=$pincorrect;
+ }
+ }
+ }
+ }
+ my $ave=$sum/(100*$number);
+# Store
+ my ($username,$domain)=split(/\:/,$user);
+ my %grades=();
+ $grades{"resource.$part.solved"}='correct_by_override';
+ $grades{"resource.$part.awarded"}=$ave;
+ $grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
+ my $returncode=&Apache::lonnet::cstore(\%grades,$symb,
+ $env{'request.course.id'},
+ $domain,$username);
+ if ($returncode ne 'ok') {
+ $result.=" Failed to save student $username:$domain. Message when trying to save was ($returncode)";
+ } else {
+ $storecount++;
+ }
+ }
+ }
+# We are done
+ $result.=' '.&mt('Successfully stored grades for [_1] student(s).',$storecount).
+ '