# The LearningOnline Network with CAPA
#
# $Id: lonproblemstatistics.pm,v 1.75 2004/03/29 18:22:28 matthew 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/
#
# (Navigate problems for statistical reports
#
###############################################
###############################################
=pod
=head1 NAME
lonproblemstatistics
=head1 SYNOPSIS
Routines to present problem statistics to instructors via tables,
Excel files, and plots.
=over 4
=cut
###############################################
###############################################
package Apache::lonproblemstatistics;
use strict;
use Apache::lonnet();
use Apache::loncommon();
use Apache::lonhtmlcommon;
use Apache::loncoursedata;
use Apache::lonstatistics;
use Apache::lonlocal;
use Spreadsheet::WriteExcel;
use Apache::lonstathelpers();
use Time::HiRes;
my @StatsArray;
##
## Localization notes:
##
## in @Fields[0]->{'long_title'} is placed in Excel files and is used as the
## header for plots created with Graph.pm, both of which more than likely do
## not support localization.
##
my @Fields = (
{ name => 'problem_num',
title => 'P#',
align => 'right',
color => '#FFFFE6' },
{ name => 'container',
title => 'Sequence or Folder',
align => 'left',
color => '#FFFFE6',
sortable => 'yes' },
{ name => 'title',
title => 'Title',
align => 'left',
color => '#FFFFE6',
special => 'link',
sortable => 'yes', },
{ name => 'part',
title => 'Part',
align => 'left',
color => '#FFFFE6',
},
{ name => 'num_students',
title => '#Stdnts',
align => 'right',
color => '#EEFFCC',
format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Number of Students Attempting Problem' },
{ name => 'tries',
title => 'Tries',
align => 'right',
color => '#EEFFCC',
format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Total Number of Tries' },
{ name => 'max_tries',
title => 'Max Tries',
align => 'right',
color => '#DDFFFF',
format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Maximum Number of Tries' },
{ name => 'min_tries',
title => 'Min Tries',
align => 'right',
color => '#DDFFFF',
format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Minumum Number of Tries' },
{ name => 'mean_tries',
title => 'Mean Tries',
align => 'right',
color => '#DDFFFF',
format => '%5.2f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Average Number of Tries' },
{ name => 'std_tries',
title => 'S.D. tries',
align => 'right',
color => '#DDFFFF',
format => '%5.2f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Standard Deviation of Number of Tries' },
{ name => 'skew_tries',
title => 'Skew Tries',
align => 'right',
color => '#DDFFFF',
format => '%5.2f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Skew of Number of Tries' },
{ name => 'num_solved',
title => '#YES',
align => 'right',
color => '#FFDDDD',
format => '%4.1f',# format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Number of Students able to Solve' },
{ name => 'num_override',
title => '#yes',
align => 'right',
color => '#FFDDDD',
format => '%4.1f',# format => '%d',
sortable => 'yes',
graphable => 'yes',
long_title => 'Number of Students given Override' },
{ name => 'num_wrong',
title => '#Wrng',
align => 'right',
color => '#FFDDDD',
format => '%4.1f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Percent of students whose final answer is wrong' },
{ name => 'deg_of_diff',
title => 'DoDiff',
align => 'right',
color => '#FFFFE6',
format => '%5.2f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Degree of Difficulty'.
'[ 1 - ((#YES+#yes) / Tries) ]'},
{ name => 'deg_of_disc',
title => 'DoDisc',
align => 'right',
color => '#FFFFE6',
format => '%4.2f',
sortable => 'yes',
graphable => 'yes',
long_title => 'Degree of Discrimination' },
);
###############################################
###############################################
=pod
=item &CreateInterface()
Create the main intereface for the statistics page. Allows the user to
select sections, maps, and output.
=cut
###############################################
###############################################
sub CreateInterface {
my $Str = '';
$Str .= &Apache::lonhtmlcommon::breadcrumbs
(undef,'Overall Problem Statistics','Statistics_Overall_Key');
$Str .= '
';
#
my $only_seq_with_assessments = sub {
my $s=shift;
if ($s->{'num_assess'} < 1) {
return 0;
} else {
return 1;
}
};
$Str .= &Apache::lonstatistics::MapSelect('Maps','multiple,all',5,
$only_seq_with_assessments);
$Str .= '
'."\n";
$Str .= '
'."\n";
$Str .= '';
$Str .= ' 'x5;
$Str .= 'Plot '.&plot_dropdown().(' 'x10);
$Str .= '';
$Str .= ' 'x5;
$Str .= '';
$Str .= ' 'x5;
$Str .= '';
$Str .= ' 'x5;
return $Str;
}
###############################################
###############################################
=pod
=item &BuildProblemStatisticsPage()
Main interface to problem statistics.
=cut
###############################################
###############################################
sub BuildProblemStatisticsPage {
my ($r,$c)=@_;
#
my %Saveable_Parameters = ('Status' => 'scalar',
'statsoutputmode' => 'scalar',
'Section' => 'array',
'StudentData' => 'array',
'Maps' => 'array');
&Apache::loncommon::store_course_settings('statistics',
\%Saveable_Parameters);
&Apache::loncommon::restore_course_settings('statistics',
\%Saveable_Parameters);
#
&Apache::lonstatistics::PrepareClasslist();
#
# Clear the package variables
undef(@StatsArray);
#
# Finally let the user know we are here
my $interface = &CreateInterface();
$r->print($interface);
$r->print('');
#
if (! exists($ENV{'form.statsfirstcall'})) {
$r->print('');
$r->print('
'.
&mt('Press "Generate Statistics" when you are ready.').
'
'.
&mt('It may take some time to update the student data '.
'for the first analysis. Future analysis this session '.
' will not have this delay.').
'
');
return;
} elsif ($ENV{'form.statsfirstcall'} eq 'yes' ||
exists($ENV{'form.UpdateCache'}) ||
exists($ENV{'form.ClearCache'}) ) {
$r->print('');
&Apache::lonstatistics::Gather_Student_Data($r);
} else {
$r->print('');
}
$r->rflush();
#
# This probably does not need to be done each time we are called, but
# it does not slow things down noticably.
&Apache::loncoursedata::populate_weight_table();
#
if (exists($ENV{'form.Excel'})) {
&Excel_output($r);
} else {
my $count = 0;
foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
$count += $seq->{'num_assess'};
}
if ($count > 10) {
$r->print('
'.
&mt('Compiling statistics for [_1] problems',$count).
'
');
if ($count > 30) {
$r->print('
'.&mt('This will take some time.').'
');
}
$r->rflush();
}
#
my $sortby = $ENV{'form.sortby'};
$sortby = 'container' if (! defined($sortby) || $sortby =~ /^\s*$/);
my $plot = $ENV{'form.plot'};
if ($plot eq '' || $plot eq 'none') {
undef($plot);
}
if ($sortby eq 'container' && ! defined($plot)) {
&output_html_by_sequence($r);
} else {
if (defined($plot)) {
&make_plot($r,$plot);
}
&output_html_stats($r);
}
}
return;
}
##########################################################
##########################################################
##
## HTML output routines
##
##########################################################
##########################################################
sub output_html_by_sequence {
my ($r) = @_;
my $c = $r->connection();
$r->print(&html_preamble());
#
foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
last if ($c->aborted);
next if ($seq->{'num_assess'} < 1);
$r->print("
".$seq->{'title'}."
".
'
'."\n".
'
'."\n".
'
'.
&statistics_table_header('no container')."
\n");
my @Data = &compute_statistics_on_sequence($seq);
foreach my $data (@Data) {
$r->print('
\n";
my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
if (defined($starttime) || defined($endtime)) {
# Inform the user what the time limits on the data are.
$Str .= '
'.&mt('Statistics on submissions from [_1] to [_2]',
&Apache::lonlocal::locallocaltime($starttime),
&Apache::lonlocal::locallocaltime($endtime)
).'
';
}
$Str .= "
".&mt('Compiled on [_1]',
&Apache::lonlocal::locallocaltime(time))."
";
return $Str;
}
###############################################
###############################################
##
## Misc HTML output routines
##
###############################################
###############################################
sub statistics_html_table_data {
my ($data,$options) = @_;
my $row = '';
foreach my $field (@Fields) {
next if ($options =~ /no $field->{'name'}/);
$row .= '
';
}
return $row;
}
sub statistics_table_header {
my ($options) = @_;
my $header_row;
foreach my $field (@Fields) {
next if ($options =~ /no $field->{'name'}/);
$header_row .= '
\n");
return;
}
sub degrees_plot {
my ($r)=@_;
my $count = scalar(@StatsArray);
my $width = 50 + 10*$count;
$width = 300 if ($width < 300);
my $height = 300;
my $plot = '';
my $ymax = 0;
my $ymin = 0;
my @Disc; my @Diff; my @Labels;
foreach my $data (@StatsArray) {
push(@Labels,$data->{'problem_num'});
my $disc = $data->{'deg_of_disc'};
my $diff = $data->{'deg_of_diff'};
push(@Disc,$disc);
push(@Diff,$diff);
#
$ymin = $disc if ($ymin > $disc);
$ymin = $diff if ($ymin > $diff);
$ymax = $disc if ($ymax < $disc);
$ymax = $diff if ($ymax < $diff);
}
#
# Make sure we show relevant information.
if ($ymin < 0) {
if (abs($ymin) < 0.05) {
$ymin = 0;
} else {
$ymin = -1;
}
}
if ($ymax > 0) {
if (abs($ymax) < 0.05) {
$ymax = 0;
} else {
$ymax = 1;
}
}
#
my $xmax = $Labels[-1];
if ($xmax > 50) {
if ($xmax % 10 != 0) {
$xmax = 10 * (int($xmax/10)+1);
}
} else {
if ($xmax % 5 != 0) {
$xmax = 5 * (int($xmax/5)+1);
}
}
#
my $discdata .= ''.join(',',@Labels).''.$/.
''.join(',',@Disc).''.$/;
#
my $diffdata .= ''.join(',',@Labels).''.$/.
''.join(',',@Diff).''.$/;
#
$plot=<<"END";
Degree of Discrmination and Degree of DifficultyProblem Number
$discdata
$diffdata
END
my $plotresult =
'
'.&Apache::lonxml::xmlparse($r,'web',$plot).'
'.$/;
$r->print($plotresult);
return;
}
sub tries_data_plot {
my ($r)=@_;
my $count = scalar(@StatsArray);
my $width = 50 + 10*$count;
$width = 300 if ($width < 300);
my $height = 300;
my $plot = '';
my @STD; my @Mean; my @Max; my @Min;
my @Labels;
my $ymax = 5;
foreach my $data (@StatsArray) {
my $max = $data->{'mean_tries'} + $data->{'std_tries'};
$ymax = $max if ($ymax < $max);
$ymax = $max if ($ymax < $max);
push(@Labels,$data->{'problem_num'});
push(@STD,$data->{'std_tries'});
push(@Mean,$data->{'mean_tries'});
}
#
# Make sure we show relevant information.
my $xmax = $Labels[-1];
if ($xmax > 50) {
if ($xmax % 10 != 0) {
$xmax = 10 * (int($xmax/10)+1);
}
} else {
if ($xmax % 5 != 0) {
$xmax = 5 * (int($xmax/5)+1);
}
}
$ymax = int($ymax)+1+2;
#
my $std_data .= ''.join(',',@Labels).''.$/.
''.join(',',@Mean).''.$/;
#
my $std_error_data .= ''.join(',',@Labels).''.$/.
''.join(',',@Mean).''.$/.
''.join(',',@STD).''.$/;
#
$plot=<<"END";
Mean and S.D. of TriesProblem Number
$std_error_data
$std_data
END
my $plotresult =
'
'.&Apache::lonxml::xmlparse($r,'web',$plot).'
'.$/;
$r->print($plotresult);
return;
}
sub plot_dropdown {
my $current = '';
#
if (defined($ENV{'form.plot'})) {
$current = $ENV{'form.plot'};
}
#
my @Additional_Plots = (
{ graphable=>'yes',
name => 'degrees',
title => 'DoDisc and DoDiff' },
{ graphable=>'yes',
name => 'tries statistics',
title => 'Mean and S.D. of Tries' });
#
my $Str= "\n".''."\n";
return $Str;
}
###############################################
###############################################
##
## Excel output routines
##
###############################################
###############################################
sub Excel_output {
my ($r) = @_;
$r->print('
'.&mt('Preparing Excel Spreadsheet').'
');
##
## Compute the statistics
&compute_all_statistics($r);
my $c = $r->connection;
return if ($c->aborted());
##
## Create the excel workbook
my $filename = '/prtspool/'.
$ENV{'user.name'}.'_'.$ENV{'user.domain'}.'_'.
time.'_'.rand(1000000000).'.xls';
my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
#
# Create sheet
my $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(&mt("Problems creating new Excel file. ".
"This error has been logged. ".
"Please alert your LON-CAPA administrator."));
return 0;
}
#
# 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);
#
# Add a worksheet
my $sheetname = $ENV{'course.'.$ENV{'request.course.id'}.'.description'};
if (length($sheetname) > 31) {
$sheetname = substr($sheetname,0,31);
}
my $excel_sheet = $excel_workbook->addworksheet(
&Apache::loncommon::clean_excel_name($sheetname));
##
## Begin creating excel sheet
##
my ($rows_output,$cols_output) = (0,0);
#
# Put the course description in the header
$excel_sheet->write($rows_output,$cols_output++,
$ENV{'course.'.$ENV{'request.course.id'}.'.description'});
$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($rows_output,$cols_output++,$sectionstring);
$cols_output += scalar(@Sections);
#
# Time restrictions
my $time_string;
if (defined($starttime)) {
# call localtime but not lonlocal:locallocaltime because excel probably
# cannot handle localized text. Probably.
$time_string .= 'Data collected from '.localtime($time_string);
if (defined($endtime)) {
$time_string .= ' to '.localtime($endtime);
}
$time_string .= '.';
} elsif (defined($endtime)) {
# See note above about lonlocal:locallocaltime
$time_string .= 'Data collected before '.localtime($endtime).'.';
}
#
# Put the date in there too
$excel_sheet->write($rows_output,$cols_output++,
'Compiled on '.localtime(time));
#
$rows_output++;
$cols_output=0;
#
# Long Headers
foreach my $field (@Fields) {
next if ($field->{'name'} eq 'problem_num');
if (exists($field->{'long_title'})) {
$excel_sheet->write($rows_output,$cols_output++,
$field->{'long_title'});
} else {
$excel_sheet->write($rows_output,$cols_output++,'');
}
}
$rows_output++;
$cols_output=0;
# Brief headers
foreach my $field (@Fields) {
next if ($field->{'name'} eq 'problem_num');
# Use english for excel as I am not sure how well excel handles
# other character sets....
$excel_sheet->write($rows_output,$cols_output++,$field->{'title'});
}
$rows_output++;
foreach my $data (@StatsArray) {
$cols_output=0;
foreach my $field (@Fields) {
next if ($field->{'name'} eq 'problem_num');
$excel_sheet->write($rows_output,$cols_output++,
$data->{$field->{'name'}});
}
$rows_output++;
}
#
$excel_workbook->close();
#
# Tell the user where to get their excel file
$r->print(' '.
''.
&mt('Your Excel Spreadsheet').''."\n");
$r->rflush();
return;
}
##################################################
##################################################
##
## Statistics Gathering and Manipulation Routines
##
##################################################
##################################################
sub compute_statistics_on_sequence {
my ($seq) = @_;
my @Data;
foreach my $res (@{$seq->{'contents'}}) {
next if ($res->{'type'} ne 'assessment');
foreach my $part (@{$res->{'parts'}}) {
#
# This is where all the work happens
my $data = &get_statistics($seq,$res,$part,scalar(@StatsArray)+1);
push (@Data,$data);
push (@StatsArray,$data);
}
}
return @Data;
}
sub compute_all_statistics {
my ($r) = @_;
if (@StatsArray > 0) {
# Assume we have already computed the statistics
return;
}
my $c = $r->connection;
foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
last if ($c->aborted);
next if ($seq->{'num_assess'} < 1);
&compute_statistics_on_sequence($seq);
}
}
sub sort_data {
my ($sortkey) = @_;
return if (! @StatsArray);
#
# Sort the data
my $sortby = undef;
foreach my $field (@Fields) {
if ($sortkey eq $field->{'name'}) {
$sortby = $field->{'name'};
}
}
if (! defined($sortby) || $sortby eq '' || $sortby eq 'problem_num') {
$sortby = 'container';
}
if ($sortby ne 'container') {
# $sortby is already defined, so we can charge ahead
if ($sortby =~ /^(title|part)$/i) {
# Alpha comparison
@StatsArray = sort {
lc($a->{$sortby}) cmp lc($b->{$sortby}) ||
lc($a->{'title'}) cmp lc($b->{'title'}) ||
lc($a->{'part'}) cmp lc($b->{'part'});
} @StatsArray;
} else {
# Numerical comparison
@StatsArray = sort {
my $retvalue = 0;
if ($b->{$sortby} eq 'nan') {
if ($a->{$sortby} ne 'nan') {
$retvalue = -1;
} else {
$retvalue = 0;
}
}
if ($a->{$sortby} eq 'nan') {
if ($b->{$sortby} ne 'nan') {
$retvalue = 1;
}
}
if ($retvalue eq '0') {
$retvalue = $b->{$sortby} <=> $a->{$sortby} ||
lc($a->{'title'}) <=> lc($b->{'title'}) ||
lc($a->{'part'}) <=> lc($b->{'part'});
}
$retvalue;
} @StatsArray;
}
}
#
# Renumber the data set
my $count;
foreach my $data (@StatsArray) {
$data->{'problem_num'} = ++$count;
}
return;
}
########################################################
########################################################
=pod
=item &get_statistics()
Wrapper routine from the call to loncoursedata::get_problem_statistics.
Calls lonstathelpers::get_time_limits() to limit the data set by time
and &compute_discrimination_factor
Inputs: $sequence, $resource, $part, $problem_num
Returns: Hash reference with statistics data from
loncoursedata::get_problem_statistics.
=cut
########################################################
########################################################
sub get_statistics {
my ($sequence,$resource,$part,$problem_num) = @_;
#
my ($starttime,$endtime) = &Apache::lonstathelpers::get_time_limits();
my $symb = $resource->{'symb'};
my $courseid = $ENV{'request.course.id'};
#
my $data = &Apache::loncoursedata::get_problem_statistics
(\@Apache::lonstatistics::SelectedSections,
$Apache::lonstatistics::enrollment_status,
$symb,$part,$courseid,$starttime,$endtime);
$data->{'part'} = $part;
$data->{'problem_num'} = $problem_num;
$data->{'container'} = $sequence->{'title'};
$data->{'title'} = $resource->{'title'};
$data->{'title.link'} = $resource->{'src'}.'?symb='.
&Apache::lonnet::escape($resource->{'symb'});
#
$data->{'deg_of_disc'} = &compute_discrimination_factor($resource,$part,$sequence);
return $data;
}
###############################################
###############################################
=pod
=item &compute_discrimination_factor()
Inputs: $Resource, $Sequence
Returns: integer between -1 and 1
=cut
###############################################
###############################################
sub compute_discrimination_factor {
my ($resource,$part,$sequence) = @_;
my @Resources;
foreach my $res (@{$sequence->{'contents'}}) {
next if ($res->{'symb'} eq $resource->{'symb'});
push (@Resources,$res->{'symb'});
}
#
# rank
my $ranking =
&Apache::loncoursedata::rank_students_by_scores_on_resources
(\@Resources,
\@Apache::lonstatistics::SelectedSections,
$Apache::lonstatistics::enrollment_status,undef);
#
# compute their percent scores on the problems in the sequence,
my $number_to_grab = int(scalar(@{$ranking})/4);
my $num_students = scalar(@{$ranking});
my @BottomSet = map { $_->[&Apache::loncoursedata::RNK_student()];
} @{$ranking}[0..$number_to_grab];
my @TopSet =
map {
$_->[&Apache::loncoursedata::RNK_student()];
} @{$ranking}[($num_students-$number_to_grab)..($num_students-1)];
my ($bottom_sum,$bottom_max) =
&Apache::loncoursedata::get_sum_of_scores($resource,$part,\@BottomSet);
my ($top_sum,$top_max) =
&Apache::loncoursedata::get_sum_of_scores($resource,$part,\@TopSet);
my $deg_of_disc;
if ($top_max == 0 || $bottom_max==0) {
$deg_of_disc = 'nan';
} else {
$deg_of_disc = ($top_sum/$top_max) - ($bottom_sum/$bottom_max);
}
#&Apache::lonnet::logthis(' '.$top_sum.'/'.$top_max.
# ' - '.$bottom_sum.'/'.$bottom_max);
return $deg_of_disc;
}
###############################################
###############################################
=pod
=item ProblemStatisticsLegend
=over 4
=item #Stdnts
Total number of students attempted the problem.
=item Tries
Total number of tries for solving the problem.
=item Max Tries
Largest number of tries for solving the problem by a student.
=item Mean
Average number of tries. [ Tries / #Stdnts ]
=item #YES
Number of students solved the problem correctly.
=item #yes
Number of students solved the problem by override.
=item %Wrong
Percentage of students who tried to solve the problem
but is still incorrect. [ 100*((#Stdnts-(#YES+#yes))/#Stdnts) ]
=item DoDiff
Degree of Difficulty of the problem.
[ 1 - ((#YES+#yes) / Tries) ]
=item S.D.
Standard Deviation of the tries.
[ sqrt(sum((Xi - Mean)^2)) / (#Stdnts-1)
where Xi denotes every student\'s tries ]
=item Skew.
Skewness of the students tries.
[(sqrt( sum((Xi - Mean)^3) / #Stdnts)) / (S.D.^3)]
=item Dis.F.
Discrimination Factor: A Standard for evaluating the
problem according to a Criterion
=item [Criterion to group students into %27 Upper Students -
and %27 Lower Students]
1st Criterion for Sorting the Students:
Sum of Partial Credit Awarded / Total Number of Tries
2nd Criterion for Sorting the Students:
Total number of Correct Answers / Total Number of Tries
=item Disc.
Number of Students had at least one discussion.
=back
=cut
############################################################
############################################################
##
## How this all works:
## Statistics are computed by calling &get_statistics with the sequence,
## resource, and part id to run statistics on. At various places within
## the loops which compute the statistics, as well as before and after
## the entire process, subroutines can be called. The subroutines are
## registered to the following hooks:
##
## hook subroutine inputs
## ----------------------------------------------------------
## pre $r,$count
## pre_seq $r,$count,$seq
## pre_res $r,$count,$seq,$res
## calc $r,$count,$seq,$res,$data
## post_res $r,$count,$seq,$res
## post_seq $r,$count,$seq
## post $r,$count
##
## abort $r
##
## subroutines will be called in the order in which they are registered.
##
############################################################
############################################################
{
my %hooks;
my $aborted = 0;
sub abort_computation {
$aborted = 1;
}
sub clear_hooks {
$aborted = 0;
undef(%hooks);
}
sub register_hook {
my ($hookname,$subref)=@_;
if ($hookname !~ /^(pre|pre_seq|pre_res|post|post_seq|post_res|calc)$/){
return;
}
if (ref($subref) ne 'CODE') {
&Apache::lonnet::logthis('attempt to register hook to non-code: '.
$hookname,' = '.$subref);
} else {
if (exists($hooks{$hookname})) {
push(@{$hooks{$hookname}},$subref);
} else {
$hooks{$hookname} = [$subref];
}
}
return;
}
sub run_hooks {
my $context = shift();
foreach my $hook (@{$hooks{$context}}) {
if ($aborted && $context ne 'abort') {
last;
}
my $retvalue = $hook->(@_);
if (defined($retvalue) && $retvalue eq '0') {
$aborted = 1 if (! $aborted);
}
}
}
sub run_statistics {
my ($r) = @_;
my $count = 0;
&run_hooks('pre',$r,$count);
foreach my $seq (&Apache::lonstatistics::Sequences_with_Assess()) {
last if ($aborted);
next if ($seq->{'num_assess'}<1);
&run_hooks('pre_seq',$r,$count,$seq);
foreach my $res (@{$seq->{'contents'}}) {
last if ($aborted);
next if ($res->{'type'} ne 'assessment');
&run_hooks('pre_res',$r,$count,$seq,$res);
foreach my $part (@{$res->{'parts'}}) {
last if ($aborted);
#
# This is where all the work happens
my $data = &get_statistics($seq,$res,$part,++$count);
&run_hooks('calc',$r,$count,$seq,$res,$part,$data);
}
&run_hooks('post_res',$r,$count,$seq,$res);
}
&run_hooks('post_seq',$r,$count,$seq);
}
if ($aborted) {
&run_hooks('abort',$r);
} else {
&run_hooks('post',$r,$count);
}
return;
}
} # End of %hooks scope
############################################################
############################################################
1;
__END__