--- loncom/interface/statistics/lonstudentassessment.pm 2005/08/26 21:53:23 1.125 +++ loncom/interface/statistics/lonstudentassessment.pm 2009/05/17 23:13:49 1.155 @@ -1,6 +1,6 @@ # The LearningOnline Network with CAPA # -# $Id: lonstudentassessment.pm,v 1.125 2005/08/26 21:53:23 albertel Exp $ +# $Id: lonstudentassessment.pm,v 1.155 2009/05/17 23:13:49 bisitz Exp $ # # Copyright Michigan State University Board of Trustees # @@ -50,15 +50,20 @@ 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(); +use lib '/home/httpd/lib/perl/'; +use LONCAPA; + ####################################################### ####################################################### @@ -145,6 +150,7 @@ sub BuildStudentAssessmentPage { 'chartoutputmode' => 'scalar', 'chartoutputdata' => 'scalar', 'Section' => 'array', + 'Groups' => 'array', 'StudentData' => 'array', 'Maps' => 'array'); &Apache::loncommon::store_course_settings('chart',\%Saveable_Parameters); @@ -242,14 +248,14 @@ sub next_and_previous_buttons { if (defined($previous)) { my $sname = $previous->{'username'}.':'.$previous->{'domain'}; $previousbutton .= ''; } else { $previousbutton .= ''; + &mt('Previous Student').'" disabled="disabled" />'; } # # Build the next student link @@ -258,19 +264,19 @@ sub next_and_previous_buttons { if (defined($next)) { my $sname = $next->{'username'}.':'.$next->{'domain'}; $nextbutton .= ''; } else { $nextbutton .= ''; + &mt('Next Student').'" disabled="disabled" />'; } # # Build the 'all students' button my $all = ''; - $all .= ''; $Str .= $previousbutton.(' 'x5).$all.(' 'x5).$nextbutton; @@ -312,14 +318,24 @@ the chart page. ####################################################### sub CreateInterface { my $Str = ''; - $Str .= &Apache::lonhtmlcommon::breadcrumbs(undef,'Chart'); + $Str .= &Apache::lonhtmlcommon::breadcrumbs('Chart','Chart_Description:Chart_Sections:Chart_Student_Data:Chart_Enrollment_Status:Chart_Sequences:Chart_Output_Formats:Chart_Output_Data'); # $Str .= &CreateLegend(); $Str .= ''."\n"; $Str .= ''; - $Str .= ''; - $Str .= ''; - $Str .= ''; - $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= ''; + $Str .= ''; $Str .= ''; @@ -331,6 +347,8 @@ sub CreateInterface { $Str .= ''. + &Apache::loncommon::end_data_table_row()."\n"; + $Str .= &Apache::loncommon::end_data_table()."\n"; return $Str; } @@ -1144,9 +1248,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 @@ -1231,7 +1337,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; @@ -1255,7 +1361,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); @@ -1333,14 +1439,10 @@ sub excel_initialize { if ($env{'form.SelectedStudent'}) { $studentcount = '1'; } - if ($studentcount > 1) { - $r->print('

'.&mt('Compiling Excel spreadsheet for [_1] students', - $studentcount)."

\n"); - } else { - $r->print('

'. - &mt('Compiling Excel spreadsheet for 1 student'). - "

\n"); - } + $r->print('

' + .&mt('Compiling Excel spreadsheet for [quant,_1,student]...',$studentcount) + ."

\n" + ); $r->rflush(); # # Initialize progress window @@ -1366,7 +1468,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); @@ -1405,14 +1507,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 { @@ -1438,7 +1542,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++; @@ -1486,7 +1590,7 @@ sub excel_finish { # # Tell the user where to get their excel file $r->print('
'. - 'Your Excel spreadsheet.'."\n"); + ''.&mt('Your Excel spreadsheet').''."\n"); $r->rflush(); &excel_cleanup(); return; @@ -1646,7 +1750,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).'",'; @@ -1674,7 +1778,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'}) { @@ -1728,7 +1833,7 @@ sub csv_finish { # # Tell the user where to get their csv file $r->print('
'. - ''.&mt('Your csv file.').''."\n"); + ''.&mt('Your CSV file.').''."\n"); $r->rflush(); &csv_cleanup(); return; @@ -1737,6 +1842,18 @@ sub csv_finish { } +# 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"; +} + ####################################################### ####################################################### @@ -1802,12 +1919,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; } @@ -1847,14 +1964,12 @@ sub student_tries_on_sequence { # if ( ($links eq 'yes' && $symbol ne ' ') || ($links eq 'all')) { - if (length($symbol) > 1) { - &Apache::lonnet::logthis('length of symbol "'.$symbol.'" > 1'); - } - $symbol = ''.$symbol.''; + '&command=submission'; + $symbol = &link($symbol, $link); } $value .= $symbol; } @@ -1869,6 +1984,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"; +} + ####################################################### ####################################################### @@ -1895,7 +2041,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); @@ -1909,11 +2055,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; } @@ -1929,7 +2078,7 @@ sub student_performance_on_sequence { $hasdata = 1; } # - $partscore = $weight*$awarded; + $partscore = &Apache::grades::compute_points($weight,$awarded); if (! defined($awarded)) { $partscore = undef; } @@ -1939,7 +2088,7 @@ 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'}) && $resource_data->{'resource.'.$part.'.solved'} ne '') { @@ -1970,11 +2119,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; }
'.&mt('Sections').''.&mt('Student Data').''.&mt('Enrollment Status').''.&mt('Sequences and Folders').''.&mt('Sections').''. + &Apache::loncommon::help_open_topic("Chart_Sections"). + ''.&mt('Groups').''. + ''.&mt('Student Data'). + &Apache::loncommon::help_open_topic("Chart_Student_Data"). + ''.&mt('Access Status').''. + &Apache::loncommon::help_open_topic("Chart_Enrollment_Status"). + ''.&mt('Sequences and Folders').''. + &Apache::loncommon::help_open_topic("Chart_Sequences"). + ''.&mt('Output Format').''. &Apache::loncommon::help_open_topic("Chart_Output_Formats"). '
'."\n"; $Str .= &Apache::lonstatistics::SectionSelect('Section','multiple',5); $Str .= ''; + $Str .= &Apache::lonstatistics::GroupSelect('Group','multiple',5); + $Str .= ''; $Str .= &Apache::lonstatistics::StudentDataSelect('StudentData','multiple', 5,undef); $Str .= ''."\n"; @@ -351,11 +369,11 @@ sub CreateInterface { $Str .= ' 'x5; $Str .= ''; - $Str .= ' 'x5; + $Str .= '

'; $Str .= &mt('Status [_1]', - ''); - $Str .= '
'; + ''); + $Str .= '

'; return $Str; } @@ -472,6 +490,7 @@ my @OutputDataOptions = grand_maximum => 1, summary_table => 1, maximum_row => 1, + ignore_weight => 0, shortdesc => 'Total Score and Maximum Possible for each '. 'Sequence or Folder', longdesc => 'The score of each student as well as the '. @@ -490,6 +509,7 @@ my @OutputDataOptions = grand_maximum => 1, summary_table => 1, maximum_row => 1, + ignore_weight => 0, shortdesc => 'Score on each Problem Part', longdesc =>'The students score on each problem part, computed as'. 'the part weight * part awarded', @@ -507,6 +527,7 @@ my @OutputDataOptions = grand_maximum => 0, summary_table => 0, maximum_row => 0, + ignore_weight => 0, shortdesc => 'Number of Tries before success on each Problem Part', longdesc =>'The number of tries before success on each problem part.', non_html_notes => 'negative values indicate an incorrect problem', @@ -524,6 +545,7 @@ my @OutputDataOptions = grand_maximum => 1, summary_table => 1, maximum_row => 0, + ignore_weight => 1, shortdesc => 'Number of Problem Parts completed successfully.', longdesc => 'The Number of Problem Parts completed successfully and '. 'the maximum possible for each student', @@ -645,12 +667,25 @@ sub html_initialize { &mt('Unable to retrieve course information.'). ''); } + + # If we're showing links, show a checkbox to open in new + # windows. + if ($show_links ne 'no') { + my $labeltext = &mt('Show links in new window:'); + $r->print(<new_window = true; +

+NEW_WINDOW_CHECKBOX + } + # $r->print("

".$env{'course.'.$env{'request.course.id'}.'.description'}. "  ".localtime(time)."

"); # if ($chosen_output->{'base'} !~ /^final table/) { - $r->print("

".$chosen_output->{'shortdesc'}."

"); + $r->print("

".&mt($chosen_output->{'shortdesc'})."

"); } my $Str = "
\n";
     # First, the @StudentData fields need to be listed
@@ -678,8 +713,8 @@ sub html_initialize {
                 $width{$symb}->{'width_sum'} += 1;            
             }
 	    $total_count += &count_parts($navmap,$seq);
-            # Use 3 digits for the sum
-            $width{$symb}->{'width_sum'} += 3;
+            # Use 6 digits for the sum
+            $width{$symb}->{'width_sum'} += 6;
         }
         # Compute width of maximum
         if ($chosen_output->{'sequence_max'}) {
@@ -687,8 +722,8 @@ sub html_initialize {
                 # One digit for the '/'
                 $width{$symb}->{'width_sum'} +=1;
             }
-            # Use 3 digits for the total
-            $width{$symb}->{'width_sum'}+=3;
+            # Use 6 digits for the total
+            $width{$symb}->{'width_sum'}+=6;
         }
 	#
         if ($chosen_output->{'every_problem'}) {
@@ -714,6 +749,58 @@ sub html_initialize {
     $Str .= "
";
     $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 currentElement;
+function popup_score(element, score) {
+    popdown_score();
+    var left = getLeftOffset(element);
+    var top = getTopOffset(element);
+    var div = document.createElement("div");
+    div.className = "LC_chrt_popup";
+    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]);
+    element.className = "LC_chrt_popup_up";
+    currentElement = element;
+}
+
+function popdown_score() {
+    if (currentDiv) {
+        document.body.removeChild(currentDiv);
+    }
+    if (currentElement) {
+        currentElement.className = 'LC_chrt_popup_exists';
+    }
+    currentDiv = undefined;
+}
+
+JS
+
     #
     # Let the user know what we are doing
     my $studentcount = scalar(@Apache::lonstatistics::Students); 
@@ -757,12 +844,20 @@ sub html_outputstudent {
     my @tmp = &Apache::loncoursedata::get_current_state
         ($student->{'username'},$student->{'domain'},undef,
          $env{'request.course.id'});
-    if ((scalar @tmp > 0) && ($tmp[0] !~ /^error:/)) {
+    if ((scalar @tmp > 0) && ($tmp[0] !~ /^error:(.*)/)) {
         %StudentsData = @tmp;
-    }
-    if (scalar(@tmp) < 1) {
+    } else {
+	my $error = $1;
+	if (scalar(@tmp) < 1) {
+	    $Str .= ''
+                   .&mt('No Course Data')
+                   .''."\n";
+	} else {
+            $Str .= ''
+                   .&mt('Error getting student data ([_1])',$error)
+                   .''."\n";
+	}
         $nodata_count++;
-        $Str .= 'No Course Data'."\n";
         $r->print($Str);
         $r->rflush();
         return;
@@ -781,7 +876,8 @@ 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'} && 
@@ -789,16 +885,17 @@ sub html_outputstudent {
             $ratio .= ' ';
         }
         if ($chosen_output->{'sequence_sum'} && $score ne ' ') {
-            my $score .= sprintf("%3.0f",$score);
-            $ratio .= (' 'x(3-length($score))).$score;
+            my $score .= sprintf("%3.2f",$score);
+            $ratio .= (' 'x(6-length($score))).$score;
         } elsif($chosen_output->{'sequence_sum'}) {
-            $ratio .= ' 'x3;
+            $ratio .= ' 'x6;
         }
         if ($chosen_output->{'sequence_max'}) {
             if ($chosen_output->{'sequence_sum'}) {
                 $ratio .= '/';
             }
-            $ratio .= sprintf("%3.0f",$seq_max);
+            my $sequence_total=sprintf("%3.2f",$seq_max);
+            $ratio .= $sequence_total.(' 'x(6-length($sequence_total)));
         }
         #
         if (! $chosen_output->{'every_problem'}) {
@@ -832,8 +929,8 @@ sub html_outputstudent {
     if (! defined($score)) {
         $score = ' ' x $total_sum_width;
     } else {
-        $score = sprintf("%.0f",$score);
-        $score = (' 'x(3-length($score))).$score;
+        $score = sprintf("%.2f",$score);
+        $score = (' 'x(6-length($score))).$score;
     }
     $Str .= ' '.' 'x($total_sum_width-length($score)).$score.' / '.$max;
     $Str .= " \n";
@@ -866,12 +963,12 @@ sub html_finish {
 
 sub StudentAverageTotal {
     my $Str = '

'.&mt('Summary Tables').'

'.$/; - $Str .= ''."\n"; - $Str .= ''. + $Str .= &Apache::loncommon::start_data_table(); + $Str .= &Apache::loncommon::start_data_table_header_row(). ''. ''. ''. - ''.$/; + &Apache::loncommon::end_data_table_header_row().$/; foreach my $seq (@sequences) { my $symb = $seq->symb; my $ave; @@ -885,11 +982,13 @@ sub StudentAverageTotal { } my $max = $Statistics->{$symb}->{'max'}; $ave = sprintf("%.2f",$ave); - $Str .= ''. + $Str .= &Apache::loncommon::start_data_table_row(). + ''. ''. - ''."\n"; + ''. + &Apache::loncommon::end_data_table_row()."\n"; } - $Str .= "
'.&mt('Title').''.&mt('Average').''.&mt('Maximum').'
'.$seq->compTitle.''.$seq->compTitle.''.$ave.' '.$max.' '.'
'.$max.' '.'
\n"; + $Str .= &Apache::loncommon::end_data_table()."\n"; return $Str; } @@ -900,31 +999,36 @@ sub SingleStudentTotal { $student->{'fullname'}, $student->{'username'},$student->{'domain'}).''; $Str .= $/; - $Str .= ''."\n"; + $Str .= &Apache::loncommon::start_data_table()."\n"; $Str .= - ''. + &Apache::loncommon::start_data_table_header_row(). ''; if ($chosen_output->{'base'} eq 'tries') { $Str .= ''; } else { $Str .= ''; } - $Str .= ''."\n"; + $Str .= ''. + &Apache::loncommon::end_data_table_header_row()."\n"; my $total = 0; my $total_max = 0; foreach my $seq (@sequences) { my $value = $Statistics->{$seq->symb}->{'score'}; my $max = $Statistics->{$seq->symb}->{'max'}; - $Str .= ''. + $Str .= &Apache::loncommon::start_data_table_row(). + ''. ''. - ''."\n"; + ''. + &Apache::loncommon::end_data_table_row()."\n"; $total += $value; $total_max +=$max; } - $Str .= ''. + $Str .= &Apache::loncommon::start_data_table_row(). + ''. ''. - '\n"; - $Str .= "
'.&mt('Sequence or Folder').''.&mt('Parts Correct').''.&mt('Score').''.&mt('Maximum').'
'.&mt('Maximum').'
'.&HTML::Entities::encode($seq->compTitle).''.&HTML::Entities::encode($seq->compTitle).''.$value.''.$max.'
'.$max.'
'.&mt('Total').''.&mt('Total').''.$total.''.$total_max."
\n"; + '
'.$total_max.'