--- loncom/homework/grades.pm 2007/05/10 16:06:09 1.399
+++ loncom/homework/grades.pm 2007/10/09 10:31:21 1.448
@@ -1,7 +1,7 @@
# The LearningOnline Network with CAPA
# The LON-CAPA Grading handler
#
-# $Id: grades.pm,v 1.399 2007/05/10 16:06:09 albertel Exp $
+# $Id: grades.pm,v 1.448 2007/10/09 10:31:21 foxr Exp $
#
# Copyright Michigan State University Board of Trustees
#
@@ -41,41 +41,109 @@ use Apache::Constants qw(:common);
use Apache::lonlocal;
use Apache::lonenc;
use String::Similarity;
-use lib '/home/httpd/lib/perl';
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"
+
+my %first_bubble_line = (); # First bubble line no. for each bubble.
+
+# Save and restore the bubble lines array to the form env.
+
+
+sub save_bubble_lines {
+ &Apache::lonnet::logthis("Saving bubble_lines...");
+ foreach my $line (keys(%bubble_lines_per_response)) {
+ &Apache::lonnet::logthis("Saving form.scantron.bubblelines.$line value: $bubble_lines_per_response{$line}");
+ $env{"form.scantron.bubblelines.$line"} = $bubble_lines_per_response{$line};
+ $env{"form.scantron.first_bubble_line.$line"} =
+ $first_bubble_line{$line};
+ }
+}
+
+
+sub restore_bubble_lines {
+ my $line = 0;
+ %bubble_lines_per_response = ();
+ while ($env{"form.scantron.bubblelines.$line"}) {
+ my $value = $env{"form.scantron.bubblelines.$line"};
+ &Apache::lonnet::logthis("Restoring form.scantron.bubblelines.$line value: $value");
+ $bubble_lines_per_response{$line} = $value;
+ $first_bubble_line{$line} =
+ $env{"form.scantron.first_bubble_line.$line"};
+ $line++;
+ }
+
+}
+
+# Given the parsed scanline, get the response for
+# 'answer' number n:
+
+sub get_response_bubbles {
+ my ($parsed_line, $response) = @_;
+
+ my $bubble_line = $first_bubble_line{$response};
+ my $bubble_lines= $bubble_lines_per_response{$response};
+ my $selected = "";
+
+ for (my $bline = 0; $bline < $bubble_lines; $bline++) {
+ $selected .= $$parsed_line{"scantron.$bubble_line.answer"};
+ $bubble_line++;
+ }
+ return $selected;
+}
+
# ----- These first few routines are general use routines.----
+
+# Return the number of occurences of a pattern in a string.
+
+sub occurence_count {
+ my ($string, $pattern) = @_;
+
+ my @matches = ($string =~ /$pattern/g);
+
+ return scalar(@matches);
+}
+
+
+# Take a string known to have digits and convert all the
+# digits into letters in the range J,A..I.
+
+sub digits_to_letters {
+ my ($input) = @_;
+
+ my @alphabet = ('J', 'A'..'I');
+
+ my @input = split(//, $input);
+ my $output ='';
+ for (my $i = 0; $i < scalar(@input); $i++) {
+ if ($input[$i] =~ /\d/) {
+ $output .= $alphabet[$input[$i]];
+ } else {
+ $output .= $input[$i];
+ }
+ }
+ return $output;
+}
+
#
# --- 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); }
}
@@ -94,6 +162,7 @@ sub get_symb {
return ();
}
}
+ &Apache::lonenc::check_decrypt(\$symb);
return ($symb);
}
@@ -180,7 +249,7 @@ sub showResourceInfo {
if (exists($partsseen{$partID})) {
$result.="
";
} else {
- $result.="
";
+ $result.="
";
}
$partsseen{$partID}=1;
}
@@ -195,22 +264,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.
@@ -259,11 +360,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
';
@@ -327,7 +428,10 @@ sub cleanRecord {
$result.='';
return $result;
}
-
+ } elsif ( $response =~ m/(?:numerical|formula)/) {
+ $answer =
+ &Apache::loncommon::format_previous_attempt_value('submission',
+ $answer);
}
return $answer;
}
@@ -373,6 +477,7 @@ COMMONJSFUNCTIONS
sub getclasslist {
my ($getsec,$filterlist) = @_;
my @getsec;
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
if (!ref($getsec)) {
if ($getsec ne '' && $getsec ne 'all') {
@getsec=($getsec);
@@ -402,8 +507,8 @@ sub getclasslist {
my $status =
$classlist->{$student}->[&Apache::loncoursedata::CL_STATUS()];
# filter students according to status selected
- if ($filterlist && $env{'form.Status'} ne 'Any') {
- if ($env{'form.Status'} ne $status) {
+ if ($filterlist && (!($stu_status =~ /Any/))) {
+ if (!($stu_status =~ $status)) {
delete ($classlist->{$student});
next;
}
@@ -485,6 +590,7 @@ sub student_gradeStatus {
# Shows a student's view of problem and submission
sub jscriptNform {
my ($symb) = @_;
+ my $stu_status = join(':',&Apache::loncommon::get_env_multiple('form.Status'));
my $jscript=''."\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'));
+ my $stu_status = join(' or ',&Apache::loncommon::get_env_multiple('form.Status'));
+ $result=''.
+ &mt('There are no students in section(s) [_1] with enrollment status [_2] to modify or grade',
+ $section_display, $stu_status).
+ '';
}
$result.=&show_grading_menu_form($symb);
return $result;
@@ -3108,7 +3235,7 @@ sub viewstudentgrade {
''.
"\n".$ctr.'
'."\n";
$student=~s/:/_/; # colon doen't work in javascript for names
foreach my $apart (@$parts) {
@@ -3146,8 +3273,8 @@ sub viewstudentgrade {
$result.=' \n";
} else {
@@ -3169,9 +3296,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='
'.
@@ -3369,7 +3497,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);
}
@@ -3472,6 +3600,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
@@ -3566,6 +3695,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.=<
@@ -3773,7 +3903,6 @@ sub csvuploadassign {
}
if (! %grades) { push(@skipped,"$username:$domain no data to save"); }
$grades{"resource.regrader"}="$env{'user.name'}:$env{'user.domain'}";
-# &Apache::lonnet::logthis(" storing ".(join('-',%grades)));
my $result=&Apache::lonnet::cstore(\%grades,$symb,
$env{'request.course.id'},
$domain,$username);
@@ -3840,7 +3969,7 @@ LISTJAVASCRIPT
$result.='
'."\n";
$result.=' Problems from:
');
+
+ # 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);
@@ -5526,6 +6521,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;
@@ -5536,6 +6551,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) {
@@ -5558,6 +6590,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'});
@@ -5575,6 +6617,8 @@ sub scantron_validate_CODE {
my %allcodes=&get_codes();
+ &scantron_get_maxbubble(); # parse needs the lines per response array.
+
my ($scanlines,$scan_data)=&scantron_getfile();
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
my $line=&scantron_get_line($scanlines,$scan_data,$i);
@@ -5609,6 +6653,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
@@ -5618,6 +6671,9 @@ sub scantron_validate_doublebubble {
#get scantron line setup
my %scantron_config=&get_scantron_config($env{'form.scantron_format'});
my ($scanlines,$scan_data)=&scantron_getfile();
+
+ &scantron_get_maxbubble(); # parse needs the bubble line array.
+
for (my $i=0;$i<=$scanlines->{'count'};$i++) {
my $line=&scantron_get_line($scanlines,$scan_data,$i);
if ($line=~/^[\s\cz]*$/) { next; }
@@ -5632,32 +6688,103 @@ 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 results to $env{'form.scantron_maxbubble'},
+ $env{'form.scantron.bubble_lines.n'} and
+ $env{'form.scantron.first_bubble_line.n'}
+ which are the total number of bubble, lines, the number of bubble
+ lines for reponse n and number of the first bubble line for response n.
+
+=cut
+
sub scantron_get_maxbubble {
+ &Apache::lonnet::logthis("get_max_bubble");
if (defined($env{'form.scantron_maxbubble'}) &&
$env{'form.scantron_maxbubble'}) {
+ &Apache::lonnet::logthis("cached");
+ &restore_bubble_lines();
return $env{'form.scantron_maxbubble'};
}
+ &Apache::lonnet::logthis("computing");
- my $navmap=Apache::lonnavmaps::navmap->new();
- my (undef,undef,$sequence)=
+ my (undef, undef, $sequence) =
&Apache::lonnet::decode_symb($env{'form.selectpage'});
+ my $navmap=Apache::lonnavmaps::navmap->new();
my $map=$navmap->getResourceByUrl($sequence);
my @resources=$navmap->retrieveResources($map,\&scantron_filter,1,0);
&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 = ();
+ %first_bubble_line = ();
+
+
+ my $response_number = 0;
+ my $bubble_line = 0;
foreach my $resource (@resources) {
+ my $symb = $resource->symb();
+ &Apache::lonxml::clear_bubble_lines_for_part();
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 ($trash, $part) = split(/\./, $part_id);
+
+ my $lines = $analysis{"$part_id.bubble_lines"}[0];
+
+ # TODO - make this a persistent hash not an array.
+
+
+ $first_bubble_line{$response_number} = $bubble_line;
+ $bubble_lines_per_response{$response_number} = $lines;
+ $response_number++;
+
+ $bubble_line += $lines;
+ $total_lines += $lines;
+ }
+
}
&Apache::lonnet::delenv('scantron\.');
- $env{'form.scantron_maxbubble'} =
- &Apache::lonxml::get_problem_counter()-1;
+ &save_bubble_lines();
+ $env{'form.scantron_maxbubble'} =
+ $total_lines;
return $env{'form.scantron_maxbubble'};
}
+=pod
+
+=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.
+
+=cut
+
sub scantron_validate_missingbubbles {
my ($r,$currentphase) = @_;
#get student info
@@ -5690,6 +6817,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'});
@@ -5724,6 +6875,9 @@ SCANTRONFORM
my $start=&Time::HiRes::time();
my $i=-1;
my ($uname,$udom,$started);
+
+ &scantron_get_maxbubble(); # Need the bubble lines array to parse.
+
while ($i<$scanlines->{'count'}) {
($uname,$udom)=('','');
$i++;
@@ -5774,8 +6928,6 @@ SCANTRONFORM
}
my $result=&Apache::lonnet::ssi($resource->src(),%form);
if ($result ne '') {
- &Apache::lonnet::logthis("scantron grading error -> $result");
- &Apache::lonnet::logthis("scantron grading error info name $uname domain $udom course $env{'request.course.id'} url ".$resource->src());
}
if (&Apache::loncommon::connection_aborted($r)) { last; }
}
@@ -5794,6 +6946,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'}));
@@ -5830,6 +6990,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);
@@ -5885,6 +7054,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())) {
@@ -5893,6 +7070,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));
@@ -5929,6 +7116,12 @@ DOWNLOAD
return '';
}
+=pod
+
+=back
+
+=cut
+
#-------- end of section for handling grading scantron forms -------
#
#-------------------------------------------------------------------
@@ -5939,7 +7132,7 @@ DOWNLOAD
sub show_grading_menu_form {
my ($symb)=@_;
my $result.='
'."\n".
- ''."\n".
+ ''."\n".
''."\n".
''."\n".
''."\n".
@@ -5959,13 +7152,103 @@ sub savedState {
return \%savedState;
}
-#--- Displays the main menu page -------
-sub gradingmenu {
+sub grading_menu {
my ($request) = @_;
my ($symb)=&get_symb($request);
if (!$symb) {return '';}
my $probTitle = &Apache::lonnet::gettitle($symb);
+ my ($table,undef,$hdgrade) = &showResourceInfo($symb,$probTitle);
+
+ #
+ # Define menu data
+ $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb);
+ my ($table) = &showResourceInfo($symb,$env{'form.probTitle'});
+ $request->print($table);
+ my %fields = ('symb'=>&Apache::lonenc::check_encrypt($symb),
+ 'handgrade'=>$hdgrade,
+ 'probTitle'=>$probTitle,
+ 'command'=>'submit_options',
+ 'saveState'=>"",
+ 'gradingMenu'=>1,
+ 'showgrading'=>"yes");
+ my $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ my @menu = ({ url => $url,
+ name => &mt('Manual Grading/View Submissions'),
+ short_description =>
+ &mt('Start the process of hand grading submissions.'),
+ });
+ $fields{'command'} = 'csvform';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Upload Scores'),
+ short_description =>
+ &mt('Specify a file containing the class scores for current resource.')});
+ $fields{'command'} = 'processclicker';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Process Clicker'),
+ short_description =>
+ &mt('Specify a file containing the clicker information for this resource.')});
+ $fields{'command'} = 'scantron_selectphase';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Grade Scantron Forms'),
+ short_description =>
+ &mt('')});
+ $fields{'command'} = 'verify';
+ $url = &Apache::lonhtmlcommon::build_url('grades/',\%fields);
+ push (@menu, { url => "",
+ jscript => ' onClick="javascript:checkChoice2(document.forms.gradingMenu,\'5\',\'verify\')" ',
+ name => &mt('Verify Receipt'),
+ short_description =>
+ &mt('')});
+ $fields{'command'} = 'manage';
+ $url = &Apache::lonhtmlcommon::build_url('/adm/helper/resettimes.helper',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('Manage Access Times'),
+ short_description =>
+ &mt('')});
+ $fields{'command'} = 'view';
+ $url = &Apache::lonhtmlcommon::build_url('/adm/pickcode',\%fields);
+ push (@menu, { url => $url,
+ name => &mt('View Saved CODEs'),
+ short_description =>
+ &mt('')});
+
+ #
+ # Create the menu
+ my $Str;
+ # $Str .= '
'."\n";
return $result;
}
@@ -6125,10 +7520,493 @@ sub init_perm {
}
}
+sub gather_clicker_ids {
+ my %clicker_ids;
+
+ my $classlist = &Apache::loncoursedata::get_classlist();
+
+ # Set up a couple variables.
+ my $username_idx = &Apache::loncoursedata::CL_SNAME();
+ my $domain_idx = &Apache::loncoursedata::CL_SDOM();
+ my $status_idx = &Apache::loncoursedata::CL_STATUS();
+
+ foreach my $student (keys(%$classlist)) {
+ if ($classlist->{$student}->[$status_idx] ne 'Active') { next; }
+ my $username = $classlist->{$student}->[$username_idx];
+ my $domain = $classlist->{$student}->[$domain_idx];
+ my $clickers =
+ (&Apache::lonnet::userenvironment($domain,$username,'clickers'))[1];
+ foreach my $id (split(/\,/,$clickers)) {
+ $id=~s/^[\#0]+//;
+ $id=~s/[\-\:]//g;
+ if (exists($clicker_ids{$id})) {
+ $clicker_ids{$id}.=','.$username.':'.$domain;
+ } else {
+ $clicker_ids{$id}=$username.':'.$domain;
+ }
+ }
+ }
+ return %clicker_ids;
+}
+
+sub gather_adv_clicker_ids {
+ my %clicker_ids;
+ my $cnum=$env{'course.'.$env{'request.course.id'}.'.num'};
+ my $cdom=$env{'course.'.$env{'request.course.id'}.'.domain'};
+ my %coursepersonnel=&Apache::lonnet::get_course_adv_roles($cdom.'/'.$cnum);
+ foreach my $element (sort(keys(%coursepersonnel))) {
+ foreach my $person (split(/\,/,$coursepersonnel{$element})) {
+ my ($puname,$pudom)=split(/\:/,$person);
+ my $clickers =
+ (&Apache::lonnet::userenvironment($pudom,$puname,'clickers'))[1];
+ foreach my $id (split(/\,/,$clickers)) {
+ $id=~s/^[\#0]+//;
+ $id=~s/[\-\:]//g;
+ if (exists($clicker_ids{$id})) {
+ $clicker_ids{$id}.=','.$puname.':'.$pudom;
+ } else {
+ $clicker_ids{$id}=$puname.':'.$pudom;
+ }
+ }
+ }
+ }
+ return %clicker_ids;
+}
+
+sub clicker_grading_parameters {
+ return ('gradingmechanism' => 'scalar',
+ 'upfiletype' => 'scalar',
+ 'specificid' => 'scalar',
+ 'pcorrect' => 'scalar',
+ 'pincorrect' => 'scalar');
+}
+
+sub process_clicker {
+ my ($r)=@_;
+ my ($symb)=&get_symb($r);
+ if (!$symb) {return '';}
+ my $result=&checkforfile_js();
+ $env{'form.probTitle'} = &Apache::lonnet::gettitle($symb);
+ my ($table) = &showResourceInfo($symb,$env{'form.probTitle'});
+ $result.=$table;
+ $result.='
'."\n";
+ $result.='
'."\n";
+ $result.=' '.&mt('Specify a file containing the clicker information for this resource').
+ '.
'."\n";
+ $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(s)");
+ my $pcorrect=&mt("Percentage points for correct solution");
+ my $pincorrect=&mt("Percentage points for incorrect solution");
+ 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() {
+// Accept only integer percentages
+ document.forms.gradesupload.pcorrect.value=Math.round(document.forms.gradesupload.pcorrect.value);
+ document.forms.gradesupload.pincorrect.value=Math.round(document.forms.gradesupload.pincorrect.value);
+// Find out grading choice
+ for (i=0; i
+
'."\n";
+ $result.=&show_grading_menu_form($symb);
+ return $result;
+}
+
+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').'';
+ return $result.&show_grading_menu_form($symb);
+ }
+ my %clicker_ids=&gather_clicker_ids();
+ my %correct_ids;
+ if ($env{'form.gradingmechanism'} eq 'personnel') {
+ %correct_ids=&gather_adv_clicker_ids();
+ }
+ if ($env{'form.gradingmechanism'} eq 'specific') {
+ 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');
+ } else {
+ my $number=0;
+ $result.='
'.&mt('Correctness determined by the following IDs').'';
+ foreach my $id (sort(keys(%correct_ids))) {
+ $result.=' '.$id.' - ';
+ if ($correct_ids{$id} eq 'specified') {
+ $result.=&mt('specified');
+ } else {
+ my ($uname,$udom)=split(/\:/,$correct_ids{$id});
+ $result.=&Apache::loncommon::plainname($uname,$udom);
+ }
+ $number++;
+ }
+ $result.="
\n";
+ if ($number==0) {
+ $result.=''.&mt('No IDs found to determine correct answer').'';
+ return $result.&show_grading_menu_form($symb);
+ }
+ }
+ if (length($env{'form.upfile'}) < 2) {
+ $result.=&mt('[_1] Error: [_2] The file you attempted to upload, [_3] contained no information. Please check that you entered the correct filename.',
+ '',
+ '',
+ ''.&HTML::Entities::encode($env{'form.upfile.filename'},'<>&"').'');
+ return $result.&show_grading_menu_form($symb);
+ }
+
+# 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.=(<
+
+$heading
+
+
+
+
+
+
+
+
+ENDHEADER
+ my %responses;
+ my @questiontitles;
+ my $errormsg='';
+ my $number=0;
+ 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 corrion(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".'';
+ $correct_count++;
+ } elsif ($clicker_ids{$id}) {
+ 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".''.
+ "\n".&mt("Username").": ".
+ "\n".&mt("Domain").": ".
+ &Apache::loncommon::select_dom_form($env{'course.'.$env{'request.course.id'}.'.domain'},'udom'.$id).' '.
+ &Apache::loncommon::selectstudent_link('clickeranalysis','uname'.$id,'udom'.$id);
+ $unknown_count++;
+ }
+ }
+ $result.=''.
+ &mt('Found [_1] registered and [_2] unregistered clickers.',$student_count,$unknown_count);
+ if ($env{'form.gradingmechanism'} ne 'attendance') {
+ if ($correct_count==0) {
+ $errormsg.="Found no correct answers answers for grading!";
+ } elsif ($correct_count>1) {
+ $result.=' '.&mt("Found [_1] entries for grading!",$correct_count).'';
+ }
+ }
+ if ($number<1) {
+ $errormsg.="Found no questions.";
+ }
+ if ($errormsg) {
+ $result.=' '.&mt($errormsg).'';
+ } else {
+ $result.=' ';
+ }
+ $result.='
'."\n".
+ '
'."\n";
+ return $result.&show_grading_menu_form($symb);
+}
+
+sub iclicker_eval {
+ my ($questiontitles,$responses)=@_;
+ my $number=0;
+ my $errormsg='';
+ 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[0] eq 'Question') {
+ for (my $i=3;$i<$#entries;$i+=6) {
+ $$questiontitles[$number]=$entries[$i];
+ $number++;
+ }
+ }
+ if ($entries[0]=~/^\#/) {
+ my $id=$entries[0];
+ my @idresponses;
+ $id=~s/^[\#0]+//;
+ for (my $i=0;$i<$number;$i++) {
+ my $idx=3+$i*6;
+ push(@idresponses,$entries[$idx]);
+ }
+ $$responses{$id}=join(',',@idresponses);
+ }
+ }
+ 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).
+ '