--- loncom/interface/statistics/lonstudentassessment.pm 2005/02/25 23:32:23 1.115 +++ loncom/interface/statistics/lonstudentassessment.pm 2006/04/28 21:58:33 1.137 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonstudentassessment.pm,v 1.115 2005/02/25 23:32:23 matthew Exp $ +# $Id: lonstudentassessment.pm,v 1.137 2006/04/28 21:58:33 albertel Exp $ # # Copyright Michigan State University Board of Trustees # @@ -50,12 +50,14 @@ Presents assessment data about a student package Apache::lonstudentassessment; use strict; -use Apache::lonstatistics; -use Apache::lonhtmlcommon; +use Apache::lonstatistics(); +use Apache::lonhtmlcommon(); use Apache::loncommon(); use Apache::loncoursedata; use Apache::lonnet; # for logging porpoises use Apache::lonlocal; +use Apache::grades(); +use Apache::lonmsgdisplay(); use Time::HiRes; use Spreadsheet::WriteExcel; use Spreadsheet::WriteExcel::Utility(); @@ -153,10 +155,10 @@ sub BuildStudentAssessmentPage { &Apache::lonstatistics::PrepareClasslist(); # $single_student_mode = 0; - $single_student_mode = 1 if ($ENV{'form.SelectedStudent'}); + $single_student_mode = 1 if ($env{'form.SelectedStudent'}); &Apache::loncommon::get_unprocessed_cgi($ENV{'QUERY_STRING'}, ['selectstudent']); - if ($ENV{'form.selectstudent'}) { + if ($env{'form.selectstudent'}) { &Apache::lonstatistics::DisplayClasslist($r); return; } @@ -167,17 +169,19 @@ sub BuildStudentAssessmentPage { $r->print(&CreateInterface()); $r->print(''); $r->print(''); + $env{'form.sort'}.'" />'); $r->rflush(); # - if (! exists($ENV{'form.notfirstrun'}) && ! $single_student_mode) { + if (! exists($env{'form.notfirstrun'}) && ! $single_student_mode) { return; } + $r->print('
"; $r->print($Str); $r->rflush(); + + $r->print(<+// get the left offset of a given widget as an absolute position +function getLeftOffset (element) { + return collect(element, "offsetLeft"); +} + +// get the top offset of a given widget as an absolute position +function getTopOffset (element) { + return collect(element, "offsetTop"); +} + +function collect(element, att) { + var val = 0; + while(element) { + val += element[att]; + element = element.offsetParent; + } + return val; +} + +var currentDiv; +var oldBorder; +var currentElement; +function popup_score(element, score) { + popdown_score(); + var left = getLeftOffset(element); + var top = getTopOffset(element); + var div = document.createElement("div"); + div.style.border = "1px solid #8888FF"; + div.style.backgroundColor = "#CCCCFF"; + div.appendChild(document.createTextNode(score)); + div.style.position = "absolute"; + div.style.top = (top - 25) + "px"; + div.style.left = (left - 10) + "px"; + currentDiv = div; + document.body.insertBefore(div, document.body.childNodes[0]); + oldBorder = element.style.border; + element.style.border = "1px solid yellow"; + currentElement = element; +} + +function popdown_score() { + if (currentDiv) { + document.body.removeChild(currentDiv); + } + if (currentElement) { + currentElement.style.border = oldBorder; + } + currentDiv = undefined; +} + +JS + + # + # Let the user know what we are doing + my $studentcount = scalar(@Apache::lonstatistics::Students); + if ($env{'form.SelectedStudent'}) { + $studentcount = '1'; + } + # + # Initialize progress window + %prog_state=&Apache::lonhtmlcommon::Create_PrgWin + ($r,'HTML Chart Status', + 'HTML Chart Progress', $studentcount, + 'inline',undef,'Statistics','stats_status'); + # + &Apache::lonhtmlcommon::Update_PrgWin($r,\%prog_state, + 'Processing first student'); return; } @@ -747,7 +829,7 @@ sub html_outputstudent { my %StudentsData; my @tmp = &Apache::loncoursedata::get_current_state ($student->{'username'},$student->{'domain'},undef, - $ENV{'request.course.id'}); + $env{'request.course.id'}); if ((scalar @tmp > 0) && ($tmp[0] !~ /^error:/)) { %StudentsData = @tmp; } @@ -763,7 +845,7 @@ sub html_outputstudent { my $studentstats; my $PerformanceStr = ''; foreach my $seq (@sequences) { - &Apache::lonnet::logthis('computing student data for '.$seq->compTitle); + my $symb = $seq->symb; my ($performance,$performance_length,$score,$seq_max,$rawdata); if ($chosen_output->{'tries'}) { ($performance,$performance_length,$score,$seq_max,$rawdata) = @@ -772,10 +854,12 @@ sub html_outputstudent { } else { ($performance,$performance_length,$score,$seq_max,$rawdata) = &student_performance_on_sequence($student,\%StudentsData, - $navmap,$seq,$show_links); + $navmap,$seq,$show_links, + $chosen_output->{ignore_weight}); } my $ratio=''; - if ($chosen_output->{'every_problem'}) { + if ($chosen_output->{'every_problem'} && + $chosen_output->{'sequence_sum'}) { $ratio .= ' '; } if ($chosen_output->{'sequence_sum'} && $score ne ' ') { @@ -795,15 +879,15 @@ sub html_outputstudent { $performance = ''; $performance_length=0; } - $performance .= ' 'x($width{$seq->symb}->{'width_total'} - + $performance .= ' 'x($width{$symb}->{'width_total'} - $performance_length - - $width{$seq->symb}->{'width_sum'}). + $width{$symb}->{'width_sum'}). $ratio; # $Str .= $performance.$padding; # - $studentstats->{$seq->symb}->{'score'}= $score; - $studentstats->{$seq->symb}->{'max'} = $seq_max; + $studentstats->{$symb}->{'score'}= $score; + $studentstats->{$symb}->{'max'} = $seq_max; } # # Total it up and store the statistics info. @@ -831,6 +915,7 @@ sub html_outputstudent { $r->print($Str); # $r->rflush(); + &Apache::lonhtmlcommon::Increment_PrgWin($r,\%prog_state,'last student'); return; } @@ -848,7 +933,8 @@ sub html_finish { } } $r->rflush(); - undef($navmap); + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + &html_cleanup(); return; } @@ -939,6 +1025,7 @@ sub SingleStudentTotal { my $excel_sheet; my $excel_workbook; +my $format; my $filename; my $rows_output; @@ -954,8 +1041,7 @@ my %formula_data; my $navmap; my @sequences; -sub excel_initialize { - my ($r) = @_; +sub excel_cleanup { # undef ($excel_sheet); undef ($excel_workbook); @@ -971,6 +1057,12 @@ sub excel_initialize { # undef($navmap); undef(@sequences); +} + +sub excel_initialize { + my ($r) = @_; + + &excel_cleanup(); ($navmap,@sequences) = &Apache::lonstatistics::selected_sequences_with_assessments(); if (! ref($navmap)) { @@ -1005,9 +1097,6 @@ sub excel_initialize { } return if ($request_aborted); # - $filename = '/prtspool/'. - $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. - time.'_'.rand(1000000000).'.xls'; # $excel_workbook = undef; $excel_sheet = undef; @@ -1035,56 +1124,27 @@ sub excel_initialize { my $first_data_row = $rows_output++; # # Create sheet - $excel_workbook = Spreadsheet::WriteExcel->new('/home/httpd'.$filename); - # - # Check for errors - if (! defined($excel_workbook)) { - $r->log_error("Error creating excel spreadsheet $filename: $!"); - $r->print("Problems creating new Excel file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator"); - return ; - } - # - # The excel spreadsheet stores temporary data in files, then put them - # together. If needed we should be able to disable this (memory only). - # The temporary directory must be specified before calling 'addworksheet'. - # File::Temp is used to determine the temporary directory. - $excel_workbook->set_tempdir($Apache::lonnet::tmpdir); - # - my $format = &Apache::loncommon::define_excel_formats($excel_workbook); + ($excel_workbook,$filename,$format)= + &Apache::loncommon::create_workbook($r); + return if (! defined($excel_workbook)); # # Add a worksheet - my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + my $sheetname = $env{'course.'.$env{'request.course.id'}.'.description'}; $sheetname = &Apache::loncommon::clean_excel_name($sheetname); $excel_sheet = $excel_workbook->addworksheet($sheetname); # # Put the course description in the header $excel_sheet->write($header_row,$cols_output++, - $ENV{'course.'.$ENV{'request.course.id'}.'.description'}, + $env{'course.'.$env{'request.course.id'}.'.description'}, $format->{'h1'}); $cols_output += 3; # # Put a description of the sections listed my $sectionstring = ''; - my @Sections = @Apache::lonstatistics::SelectedSections; - if (scalar(@Sections) > 1) { - if (scalar(@Sections) > 2) { - my $last = pop(@Sections); - $sectionstring = "Sections ".join(', ',@Sections).', and '.$last; - } else { - $sectionstring = "Sections ".join(' and ',@Sections); - } - } else { - if ($Sections[0] eq 'all') { - $sectionstring = "All sections"; - } else { - $sectionstring = "Section ".$Sections[0]; - } - } - $excel_sheet->write($header_row,$cols_output++,$sectionstring, + my @Sections = &Apache::lonstatistics::get_selected_sections(); + $excel_sheet->write($header_row,$cols_output++, + &Apache::lonstatistics::section_and_enrollment_description('plaintext'), $format->{'h3'}); - $cols_output += scalar(@Sections); # # Put the date in there too $excel_sheet->write($header_row,$cols_output++, @@ -1158,9 +1218,11 @@ sub excel_initialize { ! defined($formula_data{$symb}->{'Excel:endcell'})) { $formula_data{$symb}->{'Excel:endcell'} = $formula_data{$symb}->{'Excel:startcell'}; } + + my $start = $formula_data{$symb}->{'Excel:startcell'}; + my $end = $formula_data{$symb}->{'Excel:endcell'}; $formula_data{$symb}->{'Excel:sum'}= $excel_sheet->store_formula - ('=SUM('.$formula_data{$symb}->{'Excel:startcell'}. - ':'.$formula_data{$symb}->{'Excel:endcell'}.')'); + ("=IF(COUNT($start\:$end),SUM($start\:$end),\"\")"); # Determine cell the score is held in $formula_data{$symb}->{'Excel:scorecell'} = &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell @@ -1245,7 +1307,7 @@ sub excel_initialize { $weight = 1; if ($chosen_output->{'scores'}) { $weight = &Apache::lonnet::EXT - ('resource.'.$part.'.weight',$resource->{'symb'}, + ('resource.'.$part.'.weight',$resource->symb, undef,undef,undef); if (!defined($weight) || ($weight eq '')) { $weight=1; @@ -1269,7 +1331,7 @@ sub excel_initialize { &Spreadsheet::WriteExcel::Utility::xl_rowcol_to_cell($maximum_data_row,$formula_data{$symb}->{'Excel:endcol'})); $excel_sheet->repeat_formula($maximum_data_row,$cols_output++, $formula_data{$symb}->{'Excel:sum'},undef, - %replaceCells); + %replaceCells, %replaceCells); } elsif ($chosen_output->{'sequence_sum'}) { $excel_sheet->write($maximum_data_row,$cols_output++,$max); @@ -1344,7 +1406,7 @@ sub excel_initialize { # # Let the user know what we are doing my $studentcount = scalar(@Apache::lonstatistics::Students); - if ($ENV{'form.SelectedStudent'}) { + if ($env{'form.SelectedStudent'}) { $studentcount = '1'; } if ($studentcount > 1) { @@ -1380,7 +1442,7 @@ sub excel_outputstudent { foreach my $field (@to_show) { my $value = $student->{$field}; if ($field eq 'comments') { - $value = &Apache::lonmsg::retrieve_instructor_comments + $value = &Apache::lonmsgdisplay::retrieve_instructor_comments ($student->{'username'},$student->{'domain'}); } $excel_sheet->write($rows_output,$cols_output++,$value); @@ -1391,7 +1453,7 @@ sub excel_outputstudent { my @tmp = &Apache::loncoursedata::get_current_state($student->{'username'}, $student->{'domain'}, undef, - $ENV{'request.course.id'}); + $env{'request.course.id'}); if ((scalar @tmp > 0) && ($tmp[0] !~ /^error:/)) { %StudentsData = @tmp; } @@ -1419,14 +1481,16 @@ sub excel_outputstudent { } else { ($performance,$performance_length,$score,$seq_max,$rawdata) = &student_performance_on_sequence($student,\%StudentsData, - $navmap,$seq,'no'); + $navmap,$seq,'no', + $chosen_output->{ignore_weight}); } if ($chosen_output->{'every_problem'}) { if ($chosen_output->{'correct'}) { # only indiciate if each item is correct or not foreach my $value (@$rawdata) { - # nonzero means correct - $value = 1 if ($value > 0); + # positive means correct, 0 or negative means + # incorrect + $value = $value > 0 ? 1 : 0; $excel_sheet->write($rows_output,$cols_output++,$value); } } else { @@ -1452,7 +1516,7 @@ sub excel_outputstudent { # The undef is for the format $excel_sheet->repeat_formula($rows_output,$cols_output++, $formula_data{$symb}->{'Excel:sum'},undef, - %replaceCells); + %replaceCells, %replaceCells); } elsif ($chosen_output->{'sequence_sum'}) { if ($score eq ' ') { $cols_output++; @@ -1488,14 +1552,12 @@ sub excel_outputstudent { sub excel_finish { my ($r) = @_; if ($request_aborted || ! defined($navmap) || ! defined($excel_sheet)) { + &excel_cleanup(); return; } # # Write the excel file $excel_workbook->close(); - my $c = $r->connection(); - # - return if($c->aborted()); # # Close the progress window &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); @@ -1504,6 +1566,7 @@ sub excel_finish { $r->print('
'. 'Your Excel spreadsheet.'."\n"); $r->rflush(); + &excel_cleanup(); return; } @@ -1534,10 +1597,7 @@ my %prog_state; # progress window state my $navmap; my @sequences; -sub csv_initialize{ - my ($r) = @_; - # - # Clean up +sub csv_cleanup { undef($outputfile); undef($filename); undef($request_aborted); @@ -1545,6 +1605,12 @@ sub csv_initialize{ # undef($navmap); undef(@sequences); +} + +sub csv_initialize{ + my ($r) = @_; + + &csv_cleanup(); ($navmap,@sequences) = &Apache::lonstatistics::selected_sequences_with_assessments(); if (! ref($navmap)) { @@ -1575,22 +1641,18 @@ END 'inline',undef,'Statistics','stats_status'); # # Open a file - $filename = '/prtspool/'. - $ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'. - time.'_'.rand(1000000000).'.csv'; - unless ($outputfile = Apache::File->new('>/home/httpd'.$filename)) { - $r->log_error("Couldn't open $filename for output $!"); - $r->print("Problems occured in writing the csv file. ". - "This error has been logged. ". - "Please alert your LON-CAPA administrator."); - $outputfile = undef; - } + ($outputfile,$filename) = &Apache::loncommon::create_text_file($r,'csv'); + if (! defined($outputfile)) { return ''; } # # Datestamp - my $description = $ENV{'course.'.$ENV{'request.course.id'}.'.description'}; + my $description = $env{'course.'.$env{'request.course.id'}.'.description'}; print $outputfile '"'.&Apache::loncommon::csv_translate($description).'",'. '"'.&Apache::loncommon::csv_translate(scalar(localtime(time))).'"'. "\n"; + print $outputfile '"'. + &Apache::loncommon::csv_translate + (&Apache::lonstatistics::section_and_enrollment_description()). + '"'."\n"; foreach my $item ('shortdesc','non_html_notes') { next if (! exists($chosen_output->{$item})); print $outputfile @@ -1610,7 +1672,7 @@ END &Apache::loncommon::csv_translate($seq->compTitle).'",'; my $count = 0; if ($chosen_output->{'every_problem'}) { - foreach my $res (@{$seq->{'contents'}}) { + foreach my $res (&get_resources($navmap,$seq)) { if (scalar(@{$res->parts}) < 1) { next; } @@ -1662,7 +1724,7 @@ sub csv_outputstudent { foreach my $field (@to_show) { my $value = $student->{$field}; if ($field eq 'comments') { - $value = &Apache::lonmsg::retrieve_instructor_comments + $value = &Apache::lonmsgdisplay::retrieve_instructor_comments ($student->{'username'},$student->{'domain'}); } $Str .= '"'.&Apache::loncommon::csv_translate($value).'",'; @@ -1673,7 +1735,7 @@ sub csv_outputstudent { my @tmp = &Apache::loncoursedata::get_current_state($student->{'username'}, $student->{'domain'}, undef, - $ENV{'request.course.id'}); + $env{'request.course.id'}); if ((scalar @tmp > 0) && ($tmp[0] !~ /^error:/)) { %StudentsData = @tmp; } @@ -1690,7 +1752,8 @@ sub csv_outputstudent { } else { ($performance,$performance_length,$score,$seq_max,$rawdata) = &student_performance_on_sequence($student,\%StudentsData, - $navmap,$seq,'no'); + $navmap,$seq,'no', + $chosen_output->{ignore_weight}); } if ($chosen_output->{'every_problem'}) { if ($chosen_output->{'correct'}) { @@ -1734,13 +1797,11 @@ sub csv_outputstudent { sub csv_finish { my ($r) = @_; if ($request_aborted || ! defined($navmap) || ! defined($outputfile)) { + &csv_cleanup(); return; } close($outputfile); # - my $c = $r->connection(); - return if ($c->aborted()); - # # Close the progress window &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); # @@ -1748,12 +1809,25 @@ sub csv_finish { $r->print('
'. ''.&mt('Your csv file.').''."\n"); $r->rflush(); + &csv_cleanup(); return; } } +# This function will return an HTML string including a star, with +# a mouseover popup showing the "real" value. An optional second +# argument lets you show something other than a star. +sub show_star { + my $popup = shift; + my $symbol = shift || '*'; + # Escape the popup for JS. + $popup =~ s/([^-a-zA-Z0-9:;,._ ()|!\/?=&*])/'\\' . sprintf("%lo", ord($1))/ge; + + return "$symbol"; +} + ####################################################### ####################################################### @@ -1819,12 +1893,12 @@ sub student_tries_on_sequence { if ($status eq 'excused') { $symbol = 'x'; $max--; - } elsif ($status eq 'correct_by_override') { + } elsif ($status eq 'correct_by_override' && !$resource->is_task()) { $symbol = '+'; $sum++; } elsif ($tries > 0) { if ($tries > 9) { - $symbol = '*'; + $symbol = show_star($tries); } else { $symbol = $tries; } @@ -1841,7 +1915,7 @@ sub student_tries_on_sequence { } elsif ($status eq 'incorrect_by_override') { $symbol = '-'; } elsif ($status eq 'ungraded_attempted') { - $symbol = '#'; + $symbol = 'u'; } elsif ($status eq 'incorrect_attempted' || $tries > 0) { $symbol = '.'; @@ -1867,11 +1941,12 @@ sub student_tries_on_sequence { if (length($symbol) > 1) { &Apache::lonnet::logthis('length of symbol "'.$symbol.'" > 1'); } - $symbol = ''.$symbol.''; + '&command=submission'; + $symbol = &link($symbol, $link); } $value .= $symbol; } @@ -1886,6 +1961,37 @@ sub student_tries_on_sequence { return ($Str,$performance_length,$sum,$max,\@TriesData); } +=pod + +=item &link + +Inputs: + +=over 4 + +=item $text + +=item $target + +=back + +Takes the text and creates a link to the $text that honors +the value of 'new window' if clicked on, but uses a real +'href' so middle and right clicks still work. + +$target and $text are assumed to be already correctly escaped; i.e., it +can be dumped out directly into the output stream as-is. + +=cut + +sub link { + my ($text,$target) = @_; + return + "$text"; +} + ####################################################### ####################################################### @@ -1912,7 +2018,7 @@ Inputs: ####################################################### ####################################################### sub student_performance_on_sequence { - my ($student,$studentdata,$navmap,$seq,$links) = @_; + my ($student,$studentdata,$navmap,$seq,$links,$awarded_only) = @_; $links = 'no' if (! defined($links)); my $Str = ''; # final result string my ($score,$max) = (0,0); @@ -1926,11 +2032,14 @@ sub student_performance_on_sequence { my $resource_data = $studentdata->{$symb}; foreach my $part (@{$resource->parts()}) { $partscore = undef; - my $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', - $symb, - $student->{'domain'}, - $student->{'username'}, - $student->{'section'}); + my $weight; + if (!$awarded_only){ + $weight = &Apache::lonnet::EXT('resource.'.$part.'.weight', + $symb, + $student->{'domain'}, + $student->{'username'}, + $student->{'section'}); + } if (!defined($weight) || ($weight eq '')) { $weight=1; } @@ -1946,7 +2055,7 @@ sub student_performance_on_sequence { $hasdata = 1; } # - $partscore = $weight*$awarded; + $partscore = &Apache::grades::compute_points($weight,$awarded); if (! defined($awarded)) { $partscore = undef; } @@ -1956,15 +2065,21 @@ sub student_performance_on_sequence { $symbol = sprintf("%.0f",$symbol); } if (length($symbol) > 1) { - $symbol = '*'; + $symbol = show_star($symbol); } - if (exists($resource_data->{'resource.'.$part.'.solved'})) { + if (exists($resource_data->{'resource.'.$part.'.solved'}) && + $resource_data->{'resource.'.$part.'.solved'} ne '') { my $status = $resource_data->{'resource.'.$part.'.solved'}; if ($status eq 'excused') { $symbol = 'x'; $max -= $weight; # Do not count 'excused' problems. + } elsif ($status eq 'ungraded_attempted') { + $symbol = 'u'; } $hasdata = 1; + } elsif ($resource_data->{'resource.'.$part.'.award'} eq 'DRAFT') { + $symbol = 'd'; + $hasdata = 1; } elsif (!exists($resource_data->{'resource.'.$part.'.awarded'})){ # Unsolved. Did they try? if (exists($resource_data->{'resource.'.$part.'.tries'})){ @@ -1981,11 +2096,12 @@ sub student_performance_on_sequence { push (@ScoreData,$partscore); # if ( ($links eq 'yes' && $symbol ne ' ') || ($links eq 'all')) { - $symbol = ''.$symbol.''; + '&command=submission'; + $symbol = &link($symbol, $link); } $Str .= $symbol; } @@ -2013,13 +2129,13 @@ problems. ####################################################### sub CreateLegend { my $Str = "". - " 1 correct by student in 1 try\n". - " 7 correct by student in 7 tries\n". + " digit score or number of tries to get correct ". " * correct by student in more than 9 tries\n". " + correct by hand grading or override\n". " - incorrect by override\n". " . incorrect attempted\n". - " # ungraded attempted\n". + " u ungraded attempted\n". + " d draft answer saved but not submitted\n". " not attempted (blank field)\n". " x excused". "";