# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
# $Id: grades.pm,v 1.530 2008/11/18 19:14:28 jms Exp $
#
# Copyright Michigan State University Board of Trustees
#
# This file is part of the LearningOnline Network with CAPA (LON-CAPA).
#
# LON-CAPA is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# LON-CAPA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with LON-CAPA; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# /home/httpd/html/adm/gpl.txt
#
# http://www.lon-capa.org/
#
=head1 NAME
Apache::grades
=head1 SYNOPSIS
Handles the viewing of grades.
This is part of the LearningOnline Network with CAPA project
described at http://www.lon-capa.org.
=head1 OVERVIEW
Do an ssi with retries:
While I'd love to factor out this with the vesrion in lonprintout,
that would either require a data coupling between modules, which I refuse to perpetuate (there's quite enough of that already), or would require the invention of another infrastructure
I'm not quite ready to invent (e.g. an ssi_with_retry object).
At least the logic that drives this has been pulled out into loncommon.
ssi_with_retries - Does the server side include of a resource.
if the ssi call returns an error we'll retry it up to
the number of times requested by the caller.
If we still have a proble, no text is appended to the
output and we set some global variables.
to indicate to the caller an SSI error occurred.
All of this is supposed to deal with the issues described
in LonCAPA BZ 5631 see:
http://bugs.lon-capa.org/show_bug.cgi?id=5631
by informing the user that this happened.
Parameters:
resource - The resource to include. This is passed directly, without
interpretation to lonnet::ssi.
form - The form hash parameters that guide the interpretation of the resource
retries - Number of retries allowed before giving up completely.
Returns:
On success, returns the rendered resource identified by the resource parameter.
Side Effects:
The following global variables can be set:
ssi_error - If an unrecoverable error occurred this becomes true.
It is up to the caller to initialize this to false
if desired.
ssi_error_resource - If an unrecoverable error occurred, this is the value
of the resource that could not be rendered by the ssi
call.
ssi_error_message - The error string fetched from the ssi response
in the event of an error.
=head1 HANDLER SUBROUTINE
ssi_with_retries()
=head1 SUBROUTINES
=over
=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
=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 results to $env{'form.scantron_maxbubble'},
$env{'form.scantron.bubble_lines.n'},
$env{'form.scantron.first_bubble_line.n'} and
$env{"form.scantron.sub_bubblelines.n"}
which are the total number of bubble, lines, the number of bubble
lines for response n and number of the first bubble line for response n,
and a comma separated list of numbers of bubble lines for sub-questions
(for optionresponse, matchresponse, and rankresponse items), for response n.
=item scantron_validate_missingbubbles() :
Validates all scanlines in the selected file to not have any
answers that don't have bubbles that have not been verified
to be bubble free.
=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.
=item scantron_upload_scantron_data() :
Creates the screen for adding a new bubble sheet data file to a course.
=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.
=item valid_file() :
Validates that the requested bubble data file exists in the course.
=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.
=item scantron_validate_ID() :
Validates all scanlines in the selected file to not have any
invalid or underspecified student IDs
=back
=cut
package Apache::grades;
use strict;
use Apache::style;
use Apache::lonxml;
use Apache::lonnet;
use Apache::loncommon;
use Apache::lonhtmlcommon;
use Apache::lonnavmaps;
use Apache::lonhomework;
use Apache::lonpickcode;
use Apache::loncoursedata;
use Apache::lonmsg();
use Apache::Constants qw(:common);
use Apache::lonlocal;
use Apache::lonenc;
use String::Similarity;
use LONCAPA;
use POSIX qw(floor);
my %perm=();
# These variables are used to recover from ssi errors
my $ssi_retries = 5;
my $ssi_error;
my $ssi_error_resource;
my $ssi_error_message;
sub ssi_with_retries {
my ($resource, $retries, %form) = @_;
my ($content, $response) = &Apache::loncommon::ssi_with_retries($resource, $retries, %form);
if ($response->is_error) {
$ssi_error = 1;
$ssi_error_resource = $resource;
$ssi_error_message = $response->code . " " . $response->message;
}
return $content;
}
#
# Prodcuces an ssi retry failure error message to the user:
#
sub ssi_print_error {
my ($r) = @_;
my $helpurl = &Apache::loncommon::top_nav_help('Helpdesk');
$r->print('
'.&mt('Unable to retrieve a resource from a server:').'
'.&mt('Resource:').' '.$ssi_error_resource.'
'.&mt('Error:').' '.$ssi_error_message.'
'.
&mt('It is recommended that you try again later, as this error may mean the server was just temporarily unavailable, or is down for maintenance.').'
'.
&mt('If the error persists, please contact the [_1] for assistance.',$helpurl).
'
"; } else { $result.=" | "; } $partsseen{$partID}=1; } my $display_part=&get_display_part($partID,$symb); $result.=' | '.&mt('Part: [_1]',$display_part).' '. $resID.' | '. ''.&mt('Type: [_1]',$responsetype).' | '.&mt('Handgrade: [_1]',$handgrade).' | '; } } $result.='
'; } elsif ($response eq 'match') { my %answer=&Apache::lonnet::str2hash($answer); my %grading=&Apache::lonnet::str2hash($record->{$version."resource.$partid.$respid.submissiongrading"}); my @items=&Apache::lonnet::str2array($record->{$version."resource.$partid.$respid.submissionitems"}); my ($toprow,$middlerow,$bottomrow); foreach my $foil (@$order) { my $item=shift(@items); if ($grading{$foil} == 1) { $toprow.=''. '
'. ' '.&mt('Answer').' '.$toprow.''.' '.$grayFont.&mt('Option ID').' '. $grayFont.$bottomrow.'
'; } elsif ($response eq 'radiobutton') { my %answer=&Apache::lonnet::str2hash($answer); my ($toprow,$bottomrow); my $correct = &get_radiobutton_correct_foil($partid,$respid,$symb,$uname,$udom); foreach my $foil (@$order) { if (exists($answer{$foil})) { if ($foil eq $correct) { $toprow.=''. '
'. ' '.&mt('Answer').' '.$toprow.''. ' '.$grayFont.&mt('Item ID').' '. $middlerow.''.' '.$grayFont.&mt('Option ID').' '. $bottomrow.'
'; } elsif ($response eq 'essay') { if (! exists ($env{'form.'.$symb})) { my (%keyhash) = &Apache::lonnet::dump('nohist_handgrade', $env{'course.'.$env{'request.course.id'}.'.domain'}, $env{'course.'.$env{'request.course.id'}.'.num'}); my $loginuser = $env{'user.name'}.':'.$env{'user.domain'}; $env{'form.keywords'} = $keyhash{$symb.'_keywords'} ne '' ? $keyhash{$symb.'_keywords'} : ''; $env{'form.kwclr'} = $keyhash{$loginuser.'_kwclr'} ne '' ? $keyhash{$loginuser.'_kwclr'} : 'red'; $env{'form.kwsize'} = $keyhash{$loginuser.'_kwsize'} ne '' ? $keyhash{$loginuser.'_kwsize'} : '0'; $env{'form.kwstyle'} = $keyhash{$loginuser.'_kwstyle'} ne '' ? $keyhash{$loginuser.'_kwstyle'} : ''; $env{'form.'.$symb} = 1; # so that we don't have to read it from disk for multiple sub of the same prob. } $answer =~ s-\n-'. '
'. ' '.&mt('Answer').' '.$toprow.''.' '.$grayFont.&mt('Option ID').' '. $grayFont.$bottomrow.'
'.&keywords_highlight($answer).''; } elsif ( $response eq 'organic') { my $result='Smile representation: "'.$answer.'"'; my $jme=$record->{$version."resource.$partid.$respid.molecule"}; $result.=&Apache::chemresponse::jme_img($jme,$answer,400); return $result; } elsif ( $response eq 'Task') { if ( $answer eq 'SUBMITTED') { my $files = $record->{$version."resource.$respid.$partid.bridgetask.portfiles"}; my $result = &Apache::bridgetask::file_list($files,$uname,$udom); return $result; } elsif ( grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})) ) { my @matches = grep(/^\Q$version\E.*?\.instance$/, keys(%{$record})); return join('
' .&mt('Overall result: [_1]', $record->{$version."resource.$respid.$partid.status"}) .'
'; $result .= ''. &mt('The above receipt matches the following [numerate,_1,student].',$matches). '
'. $header. $contents. &Apache::loncommon::end_data_table()."\n"; } return $string.&show_grading_menu_form($symb); } #--- 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 # on the problem page. sub listStudents { my ($request) = shift; my ($symb) = &get_symb($request); my $cdom = $env{"course.$env{'request.course.id'}.domain"}; my $cnum = $env{"course.$env{'request.course.id'}.num"}; my $getsec = $env{'form.section'} eq '' ? 'all' : $env{'form.section'}; my $getgroup = $env{'form.group'} eq '' ? 'all' : $env{'form.group'}; my $submitonly= $env{'form.submitonly'} eq '' ? 'all' : $env{'form.submitonly'}; my $viewgrade = $env{'form.showgrading'} eq 'yes' ? 'View/Grade/Regrade' : 'View'; $env{'form.probTitle'} = $env{'form.probTitle'} eq '' ? &Apache::lonnet::gettitle($symb) : $env{'form.probTitle'}; my $result='